Skip to main content

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

FieldTypeDescription
contentsarrayArray of content objects (1-50 items)

Each content object supports the same fields as individual creation. See Create Content for all available fields.

Identifiers

FieldTypeDescription
identifiersarrayArray of identifier objects (max 20)
identifiers[].typestringIdentifier type: isbn_digital, isbn_printed, uuid, ddc, external_id
identifiers[].valuestringIdentifier value (max 255 chars). Validated per type (e.g., ISBN-13 checksum)
identifiers[].is_primarybooleanMark as primary identifier (optional, defaults to false)
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.

Bulk Limitations

share_with_tenants_ids is not supported in bulk operations. See Content Sharing for sharing content across tenants.

Limits

LimitValue
Maximum items per request50
Minimum items per request1
Maximum identifiers per item20

Rate Limiting

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

EndpointLimitKeyed by
POST /api/v3/content/bulk10 RPMX-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.

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

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

FieldTypeDescription
statusstringsuccess, partial_success, or failed (see below).
totalintegerTotal number of items in request.
createdintegerNumber of items successfully created.
skippedintegerNumber of items skipped (already_exists). Counted in errors[].
failedintegerNumber of items that failed (creation_failed). Counted in errors[].
contentsarrayCreated content with id, external_id, name.
errorsarrayPer-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:

StatusMeaning
successEvery item was created (skipped == 0 AND failed == 0).
partial_successAt least one item was created or skipped, with at least one skipped or failed.
failedNo item was created or skipped (every item failed at runtime).

Each entry in errors is an object:

FieldTypeDescription
indexintegerPosition of the offending item in the request contents array.
external_idstring | nullEcho of the item's external_id when present in the request; null for items submitted with identifiers only.
fieldstring | nullThe input field the error refers to: identifiers for collisions on identifiers[]. null when the error is item-scoped (e.g., creation_failed).
codestringMachine-readable code. One of already_exists, creation_failed (see below).
messagestringHuman-readable description. Not intended to be parsed by clients.

Error codes:

CodeMeaning
already_existsOne 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_failedItem could not be created due to a runtime error. Counted in failed. Inspect message; may be transient and worth retrying.
Retrieving Full Details

The bulk response returns minimal data for performance. Use Get Content or List Content to get full details after creation.

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.

{
"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."
}
]
}
}
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.

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).

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

ErrorCause
contents field is requiredMissing contents array (422)
contents must be an arraycontents is not an array (422)
Maximum 50 contents allowed per requestExceeded 50 item limit (422)
contents.N.name is requiredItem at index N missing name (422)
contents.N.file_type is invalidInvalid 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 to check conversion_status if 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


X

Graph View