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
{
"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 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) |
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.
share_with_tenants_ids is not supported in bulk operations. See Content Sharing for sharing content across tenants.
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.
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
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": ["Jane 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": ["John 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": ["Alice Johnson"],
"publisher": ["Tech Publishing"]
}
]
}'
Mixed Content Types
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": true
}
]
}'
With Complete Metadata
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": ["Finance 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": ["Finance Expert"],
"publisher": ["Business Books Inc"],
"keywords": ["retirement", "planning", "savings"],
"bisac": [{ "code": "BUS050000" }],
"preview": true
}
]
}'
Response
Success Response (200 OK)
{
"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. |
The bulk response returns minimal data for performance. Use Get Content or List Content to get full details after creation.
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.
{
"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):
{
"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."
}
]
}
}
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.
already_exists is skipped, not failedItems 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).
Validation Errors (422)
{
"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)
{
"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
- Use unique identifiers per tenant. Types
isbn_digital,isbn_printed,uuid, andexternal_idcannot 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 indata.errors[]withcode: "already_exists"and counted inskipped. - Always inspect
data.statusanddata.errors[]before treating the request as a full success. HTTP 200 only guarantees the request was processed, not that every item was created. - Map outcomes to your import enum: treat
code: "already_exists"as "skipped" (no retry needed; idempotent), andcode: "creation_failed"as "failed" (potentially retry-able; inspectmessage). - Use
indexto correlate errors back to your request.errors[].indexis the position of the offending item in thecontents[]array you sent. - 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 to check
conversion_statusif needed.
Use Cases
Publisher Catalog Import
Import titles from an ONIX feed or catalog:
{
"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": ["Author 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:
{
"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:
{
"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 - Individual content creation
- Update Content - Modify existing content
- List Content - Query created content
- Overview - API overview