> ## 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 Operations

Create multiple content items in a single API call. Files are not processed immediately, processing is triggered on-demand when a user accesses the content (publication page or reader).

## When to Use Bulk Operations

- **Catalog imports**: Migrating content from another system
- **ONIX synchronization**: Batch ingestion from publisher feeds
- **Mass uploads**: Adding multiple titles at once
- **Scheduled releases**: Publishing multiple items simultaneously

---

## Creating Content in Bulk

### Endpoint

```
POST /api/v3/content/bulk
```

### Request Body

```json
{
  "contents": [
    {
      "name": "Book Title One",
      "file_type": "pdf",
      "lang": "en",
      "file_url": "https://example.com/files/book1.pdf",
      "identifiers": [
        {
          "type": "isbn_digital",
          "value": "978-1-234-56789-0",
          "is_primary": true
        }
      ]
    },
    {
      "name": "Book Title Two",
      "file_type": "epub",
      "lang": "en",
      "file_url": "https://example.com/files/book2.epub",
      "identifiers": [
        {
          "type": "isbn_digital",
          "value": "978-0-987-65432-1",
          "is_primary": true
        }
      ]
    }
  ]
}
```

### Request Structure

| Field      | Type  | Description                           |
| ---------- | ----- | ------------------------------------- |
| `contents` | array | Array of content objects (1-50 items) |

Each content object supports the same fields as individual creation. See [Create Content](./content-v3-create.md) for all available fields.

### Identifiers

| Field                      | Type    | Description                                                                   |
| -------------------------- | ------- | ----------------------------------------------------------------------------- |
| `identifiers`              | array   | Array of identifier objects (max 20)                                          |
| `identifiers[].type`       | string  | Identifier type: `isbn_digital`, `isbn_printed`, `uuid`, `ddc`, `external_id` |
| `identifiers[].value`      | string  | Identifier value (max 255 chars). Validated per type (e.g., ISBN-13 checksum) |
| `identifiers[].is_primary` | boolean | Mark as primary identifier (optional, defaults to `false`)                    |

:::tip Primary Identifier
The primary identifier's value is automatically synced to the `external_id` field for backward compatibility. If no identifier is marked as primary, the first one in the array is auto-promoted. Only one identifier can be marked as primary.

Types `isbn_digital`, `isbn_printed`, `uuid`, and `external_id` require **tenant-level uniqueness** — two content items cannot share the same identifier of these types. The `ddc` type does not require uniqueness.

Values are matched after normalization (lowercase, non-alphanumeric characters stripped), so `978-0-306-40615-7` and `9780306406157` are treated as the same identifier.
:::

:::note Bulk Limitations
`share_with_tenants_ids` is not supported in bulk operations. See [Content Sharing](./content-v3-sharing.md) for per-item sharing, or [Bulk Share Collections](./content-v3-bulk-share.md) to share whole collections with multiple tenants in one call.
:::

### Limits

| Limit                        | Value |
| ---------------------------- | ----- |
| Maximum items per request    | 50    |
| Minimum items per request    | 1     |
| Maximum identifiers per item | 20    |

### Rate Limiting

Bulk operations have their own rate limit, separate from the rest of the API:

| Endpoint                    | Limit  | Keyed by                               |
| --------------------------- | ------ | -------------------------------------- |
| `POST /api/v3/content/bulk` | 10 RPM | `X-User-Token` header (IP as fallback) |

Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header (seconds). Plan your imports to stay within the budget: at the maximum batch size (50 items), 10 requests per minute lets you create up to 500 items per minute per token.

:::tip Imports larger than 500 items/minute
Spread the work across multiple `X-User-Token` values, or pace requests so the per-minute window does not fill up. Hitting `429` interrupts the import; the items already created in earlier batches are not rolled back.
:::

---

## Request Examples

### Bulk PDF Import

```bash
curl -X POST "https://yourstore.publica.la/api/v3/content/bulk" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      {
        "name": "Introduction to Data Science",
        "file_type": "pdf",
        "lang": "en",
        "file_url": "https://example.com/files/data-science.pdf",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-1-111-11111-3", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 29.99 }],
        "author": [{ "first_name": "Jane", "last_name": "Smith" }],
        "publisher": ["Tech Publishing"]
      },
      {
        "name": "Machine Learning Basics",
        "file_type": "pdf",
        "lang": "en",
        "file_url": "https://example.com/files/ml-basics.pdf",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-2-222-22222-9", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 24.99 }],
        "author": [{ "first_name": "John", "last_name": "Doe" }],
        "publisher": ["Tech Publishing"]
      },
      {
        "name": "Deep Learning Advanced",
        "file_type": "pdf",
        "lang": "en",
        "file_url": "https://example.com/files/deep-learning.pdf",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-3-333-33333-5", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 34.99 }],
        "author": [{ "first_name": "Alice", "last_name": "Johnson" }],
        "publisher": ["Tech Publishing"]
      }
    ]
  }'
```

### Mixed Content Types

```bash
curl -X POST "https://yourstore.publica.la/api/v3/content/bulk" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      {
        "name": "Mystery Novel",
        "file_type": "epub",
        "lang": "en",
        "file_url": "https://example.com/files/mystery.epub",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-0-321-49826-2", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 12.99 }]
      },
      {
        "name": "Mystery Novel - Audiobook",
        "file_type": "audio",
        "lang": "en",
        "identifiers": [
          { "type": "uuid", "value": "0193b1a2-d3f4-7e87-9a01-9b21a3f4e5d6", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 19.99 }]
      },
      {
        "name": "Mystery Novel - Hardcover",
        "file_type": "physical",
        "lang": "en",
        "identifiers": [
          { "type": "isbn_printed", "value": "978-0-321-49827-9", "is_primary": true }
        ],
        "prices": [{ "currency_id": "USD", "amount": 24.99 }],
        "binding_type": "hardcover",
        "pages": 320,
        "stock_quantity": 75
      }
    ]
  }'
```

### With Complete Metadata

```bash
curl -X POST "https://yourstore.publica.la/api/v3/content/bulk" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -d '{
    "contents": [
      {
        "name": "Complete Guide to Investing",
        "file_type": "pdf",
        "lang": "en",
        "file_url": "https://example.com/files/investing.pdf",
        "cover_url": "https://example.com/covers/investing.jpg",
        "description": "<p>A comprehensive guide to personal finance...</p>",
        "published_at": "2025-01-15",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-4-444-44444-7", "is_primary": true }
        ],
        "prices": [
          { "currency_id": "USD", "amount": 19.99 },
          { "currency_id": "EUR", "amount": 17.99 }
        ],
        "author": [{ "first_name": "Finance", "last_name": "Expert" }],
        "publisher": ["Business Books Inc"],
        "keywords": ["investing", "finance", "money"],
        "bisac": [{ "code": "BUS036000" }],
        "preview": true
      },
      {
        "name": "Retirement Planning Essentials",
        "file_type": "pdf",
        "lang": "en",
        "file_url": "https://example.com/files/retirement.pdf",
        "cover_url": "https://example.com/covers/retirement.jpg",
        "description": "<p>Everything you need to know about retirement...</p>",
        "published_at": "2025-02-01",
        "identifiers": [
          { "type": "isbn_digital", "value": "978-5-555-55555-3", "is_primary": true }
        ],
        "prices": [
          { "currency_id": "USD", "amount": 14.99 },
          { "currency_id": "EUR", "amount": 12.99 }
        ],
        "author": [{ "first_name": "Finance", "last_name": "Expert" }],
        "publisher": ["Business Books Inc"],
        "keywords": ["retirement", "planning", "savings"],
        "bisac": [{ "code": "BUS050000" }],
        "preview": true
      }
    ]
  }'
```

---

## Response

### Success Response (200 OK)

```json
{
  "data": {
    "status": "success",
    "total": 3,
    "created": 3,
    "skipped": 0,
    "failed": 0,
    "contents": [
      {
        "id": "468170",
        "external_id": "978-1-111-11111-3",
        "name": "Introduction to Data Science"
      },
      {
        "id": "468171",
        "external_id": "978-2-222-22222-9",
        "name": "Machine Learning Basics"
      },
      {
        "id": "468172",
        "external_id": "978-3-333-33333-5",
        "name": "Deep Learning Advanced"
      }
    ],
    "errors": []
  }
}
```

### Response Fields

| Field      | Type    | Description                                                             |
| ---------- | ------- | ----------------------------------------------------------------------- |
| `status`   | string  | `success`, `partial_success`, or `failed` (see below).                  |
| `total`    | integer | Total number of items in request.                                       |
| `created`  | integer | Number of items successfully created.                                   |
| `skipped`  | integer | Number of items skipped (`already_exists`). Counted in `errors[]`.      |
| `failed`   | integer | Number of items that failed (`creation_failed`). Counted in `errors[]`. |
| `contents` | array   | Created content with `id`, `external_id`, `name`.                       |
| `errors`   | array   | Per-item entries for skipped and failed items. Empty array when none.   |

The `status` field summarizes the batch outcome so consumers do not have to derive it from counts:

| Status            | Meaning                                                                        |
| ----------------- | ------------------------------------------------------------------------------ |
| `success`         | Every item was created (`skipped == 0` AND `failed == 0`).                     |
| `partial_success` | At least one item was created or skipped, with at least one skipped or failed. |
| `failed`          | No item was created or skipped (every item failed at runtime).                 |

Each entry in `errors` is an object:

| Field         | Type           | Description                                                                                                                                           |
| ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index`       | integer        | Position of the offending item in the request `contents` array.                                                                                       |
| `external_id` | string \| null | Echo of the item's `external_id` when present in the request; `null` for items submitted with `identifiers` only.                                     |
| `field`       | string \| null | The input field the error refers to: `identifiers` for collisions on `identifiers[]`. `null` when the error is item-scoped (e.g., `creation_failed`). |
| `code`        | string         | Machine-readable code. One of `already_exists`, `creation_failed` (see below).                                                                        |
| `message`     | string         | Human-readable description. Not intended to be parsed by clients.                                                                                     |

Error codes:

| Code              | Meaning                                                                                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `already_exists`  | One of the item's `identifiers` matches an existing record in your tenant. Counted in `skipped`. Safe to map to "skipped" in your import logic; retrying will not change the outcome. |
| `creation_failed` | Item could not be created due to a runtime error. Counted in `failed`. Inspect `message`; may be transient and worth retrying.                                                        |

:::tip Retrieving Full Details
The bulk response returns minimal data for performance. Use [Get Content](./content-v3-get.md) or [List Content](./content-v3-list.md) to get full details after creation.
:::

:::note Deferred Processing
Bulk-created content uses deferred file processing. Files are not processed until a user accesses the content. The first access to the publication page or reader triggers file processing.
:::

---

## Partial Success Handling

Bulk operations process each item independently. Successful items are created even when other items in the same request are skipped or fail. The endpoint always returns HTTP 200 when the request was structurally accepted; the per-item outcome lives in the response body.

```json
{
  "data": {
    "status": "partial_success",
    "total": 3,
    "created": 2,
    "skipped": 1,
    "failed": 0,
    "contents": [
      {
        "id": "468170",
        "external_id": "978-1-111-11111-3",
        "name": "Introduction to Data Science"
      },
      {
        "id": "468172",
        "external_id": "978-3-333-33333-5",
        "name": "Deep Learning Advanced"
      }
    ],
    "errors": [
      {
        "index": 1,
        "external_id": null,
        "field": "identifiers",
        "code": "already_exists",
        "message": "An item with this isbn_digital identifier already exists."
      }
    ]
  }
}
```

In this example, `status` is `partial_success` because at least one item was skipped. The duplicate is reported in `errors[]` with `code: already_exists` and counted in `skipped`, separate from `failed`. The same shape applies when items fail to create at runtime (different `code`, counted in `failed`):

```json
{
  "data": {
    "status": "partial_success",
    "total": 2,
    "created": 1,
    "skipped": 0,
    "failed": 1,
    "contents": [
      {
        "id": "468170",
        "external_id": "978-1-111-11111-3",
        "name": "Introduction to Data Science"
      }
    ],
    "errors": [
      {
        "index": 1,
        "external_id": null,
        "field": null,
        "code": "creation_failed",
        "message": "Item creation failed."
      }
    ]
  }
}
```

:::warning HTTP 200 does not mean every item was created
A 2xx response means the request was structurally valid and processing completed. Always inspect `data.status`, `data.failed`, and `data.errors[]` before treating the request as fully successful. Naive monitoring that only looks at status codes will not surface per-item failures.
:::

:::info `already_exists` is `skipped`, not `failed`
Items rejected because one of their `identifiers` already exists in your tenant are counted in `skipped`, separate from `failed`. Idempotent retries (re-posting the same batch) will report the items as `already_exists` again, which is the expected outcome.
:::

---

## Error Handling

HTTP 422 is reserved for structurally invalid requests: missing required fields, intra-batch duplicates, mutually exclusive fields, exceeded `max_contents`, and similar input-shape problems. Item-level outcomes (such as an identifier that already exists in the tenant, or a creation that throws) are reported as HTTP 200 with per-item entries in `data.errors[]` (see [Partial Success Handling](#partial-success-handling)).

### Validation Errors (422)

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "contents": ["The contents field is required."],
    "contents.0.name": ["The contents.0.name field is required."],
    "contents.1.file_type": ["The selected contents.1.file type is invalid."]
  }
}
```

### Too Many Items (422)

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "contents": ["Maximum 50 contents allowed per request."]
  }
}
```

### Common Error Causes

| Error                                                 | Cause                                                                                                                           |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `contents field is required`                          | Missing `contents` array (422)                                                                                                  |
| `contents must be an array`                           | `contents` is not an array (422)                                                                                                |
| `Maximum 50 contents allowed per request`             | Exceeded 50 item limit (422)                                                                                                    |
| `contents.N.name is required`                         | Item at index N missing name (422)                                                                                              |
| `contents.N.file_type is invalid`                     | Invalid file type at index N (422)                                                                                              |
| `Duplicate identifier across batch items: ...`        | Same identifier (after normalization) appears in two items of the same request (422)                                            |
| `An item with this <type> identifier already exists.` | One of the item's `identifiers` matches an existing record in your tenant (200, `code: "already_exists"`, counted in `skipped`) |
| `Item creation failed.`                               | Item failed to create at runtime (200, `code: "creation_failed"`, counted in `failed`)                                          |

---

## Best Practices

1. **Use unique identifiers per tenant**. Types `isbn_digital`, `isbn_printed`, `uuid`, and `external_id` cannot repeat within the same tenant. Duplicates within the same batch return HTTP 422 (the whole request is rejected). Identifiers that match content already in your tenant return HTTP 200 with that item flagged in `data.errors[]` with `code: "already_exists"` and counted in `skipped`.
2. **Always inspect `data.status` and `data.errors[]`** before treating the request as a full success. HTTP 200 only guarantees the request was processed, not that every item was created.
3. **Map outcomes to your import enum**: treat `code: "already_exists"` as "skipped" (no retry needed; idempotent), and `code: "creation_failed"` as "failed" (potentially retry-able; inspect `message`).
4. **Use `index` to correlate errors back to your request**. `errors[].index` is the position of the offending item in the `contents[]` array you sent.
5. **Bulk-created content uses deferred file processing**. Files are not processed until a user first accesses the content (publication page or reader). Use [Get Content](./content-v3-get.md) to check `conversion_status` if needed.

---

## Use Cases

### Publisher Catalog Import

Import titles from an ONIX feed or catalog:

```json
{
  "contents": [
    {
      "name": "Spring 2025 Catalog Title 1",
      "file_type": "epub",
      "lang": "en",
      "file_url": "https://publisher-cdn.com/files/title1.epub",
      "cover_url": "https://publisher-cdn.com/covers/title1.jpg",
      "identifiers": [
        {
          "type": "isbn_digital",
          "value": "978-1-111-11111-3",
          "is_primary": true
        }
      ],
      "prices": [{ "currency_id": "USD", "amount": 14.99 }],
      "author": [{ "first_name": "Author", "last_name": "Name" }],
      "publisher": ["Publisher Name"],
      "published_at": "2025-03-15",
      "bisac": [{ "code": "FIC000000" }]
    }
  ]
}
```

### Format Group Creation

Create multiple formats of the same title. Each format needs its own unique identifier:

```json
{
  "contents": [
    {
      "name": "Complete JavaScript Guide - EPUB",
      "file_type": "epub",
      "lang": "en",
      "file_url": "https://example.com/js-guide.epub",
      "identifiers": [
        {
          "type": "isbn_digital",
          "value": "978-0-262-03384-8",
          "is_primary": true
        }
      ]
    },
    {
      "name": "Complete JavaScript Guide - PDF",
      "file_type": "pdf",
      "lang": "en",
      "file_url": "https://example.com/js-guide.pdf",
      "identifiers": [
        {
          "type": "isbn_digital",
          "value": "978-0-262-03385-5",
          "is_primary": true
        }
      ]
    },
    {
      "name": "Complete JavaScript Guide - Audio",
      "file_type": "audio",
      "lang": "en",
      "identifiers": [
        {
          "type": "uuid",
          "value": "0193b1a3-1234-7e87-9a01-9b21a3f4e5d7",
          "is_primary": true
        }
      ]
    }
  ]
}
```

### Free Content Distribution

Add promotional or sample content:

```json
{
  "contents": [
    {
      "name": "Sample Chapter - Book One",
      "file_type": "pdf",
      "lang": "en",
      "file_url": "https://example.com/samples/book1-ch1.pdf",
      "identifiers": [
        {
          "type": "uuid",
          "value": "0193b1a4-5678-7e87-9a01-9b21a3f4e5d8",
          "is_primary": true
        }
      ],
      "free": true,
      "free_until": "2025-12-31"
    },
    {
      "name": "Sample Chapter - Book Two",
      "file_type": "pdf",
      "lang": "en",
      "file_url": "https://example.com/samples/book2-ch1.pdf",
      "identifiers": [
        {
          "type": "uuid",
          "value": "0193b1a4-5678-7e87-9a01-9b21a3f4e5d9",
          "is_primary": true
        }
      ],
      "free": true,
      "free_until": "2025-12-31"
    }
  ]
}
```

---

## See Also

- [Create Content](./content-v3-create.md) - Individual content creation
- [Update Content](./content-v3-update.md) - Modify existing content
- [List Content](./content-v3-list.md) - Query created content
- [Overview](./content-v3-overview.md) - API overview

---
