Add Omnipubs via the API

Overview

Use this guide to add Omnipubs to Cashmere programmatically via the REST API. It covers authentication, request schema, validation rules, and example response

All content upload endpoints require Bearer token authentication. Include your API key in the Authorization header of each request. You can receive yours in the Console

Omnipub: Create an omnipub

Create an omnipub asynchronously from EPUB, HTML, PDF, XML, or Markdown. This endpoint accepts content as a file, a URL to a file, or a JSON object.

  • Method: POST
  • Path: /api/v2/omnipub
  • Auth: Async Auth Bearer (Bearer token)
  • Content type: multipart/form-data

Notable Upload Behavior

  • Direct file uploads are stored in object storage and queued for processing.
  • File URLs (publicly accessible) are fetched, stored, and queued.
  • Inline content (HTML or Markdown passed directly by the request body) is parsed and converted into a normalized Omnipub object.
  • Max file size: 100MB

API Reference

Omnipub API

Responses

  • 202 Accepted application/json: the Omnipub was created and queued for processing.
{
  "status": "success",
  "uuid": "2a4f9c2e7a1b4b9c9f2e1b9c6a2d0f3e",
  "pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}
  • 4xx application/json: the request was rejected, with the reason in detail. For example, 400 when the request sends more than one content source, or 403 when the API key's organization is not a publisher.
{
  "detail": "Access denied. Publisher privileges required."
}

cURL example

curl -X POST https://cashmere.io/api/v2/omnipub \
  -H "Authorization: Bearer $TOKEN" \
  -F external_id="9780812968255" \
  -F collection_ids=42 \
  -F file_url="https://example.com/meditations.epub" \
  -F metadata='{"title":"Meditations","authors":["Marcus Aurelius"]}'

Let curl set the Content-Type header for -F uploads. Setting it by hand drops the multipart boundary and the request fails.

Which file format should I upload?

If you only have content in a specific format, just upload that. As long as we support it, the data will be ingested. However, some formats are better than others for specific use-cases.

Format Description When to Use
EPUB Standard eBook container; includes rich metadata, structure, and assets. Ideal for final book content or professionally prepared publications.
HTML Web-native markup. Best for content exported from CMS or landing pages.
Markdown Lightweight, text-first format. Useful for internal knowledge bases, blog drafts, or quick ingestion.
PDF Page-based representation. Use if original material is only available in PDF, but note that layout extraction may be less precise than EPUB.
XML Structured data format. Use for technical documents or archives where XML schemas define structure.

Identifiers: What They Mean and Why They Matter

Identifier Example Purpose / Why You Care
external_id "9780140455113" Assigned by you. Used to link your internal record to the Omnipub. Must be unique per account or else the Omnibook is marked as duplicate (code 1100).
pub_hash "ae803b4d136ce2f6a80d64d15ad3dfc7" Cashmere-generated, unique hash based on the Omnipub content. Allows you to determine if the content you are adding is the same as content you already have in the Cashmere API. Omnibooks with identical pub_hash values are marked as duplicate (code 1100).
uuid "52e24f5a1e0f42f8af09a4a91ded8d16" Cashmere API reference value for the creation job. Use this to poll the /api/v2/omnipub/{uuid}/status endpoint.

We strongly recommend populating the external_id field when adding Omnipubs so that it is easier to identify your data in our database.

Examples of an external_id are an ISBN, blog post id, or UUID that you use to identify content on your system.

We also strongly recommend saving the pub_hash and uuid from the API for easy process polling and content lookup.

Ingestion Lifecycle & Status Codes

After a successful POST, you must poll GET /api/v2/omnipub/{uuid}/status (or GET /api/v2/omnipub/by-external-id/{external_id}/status) to verify ingestion. Each response includes status_label and status_description, which say what stage the Omnipub is in.

Status Label Description
200 Indexed Processing complete. Content is searchable via the /search endpoint. Omnipubs processed before the current status codes may report 100 (Ready); treat both as done.
145 Held for license Processed, but not indexed until a license is approved.
330, 340 Error, Failed reparse Ingestion failed. Check the error_message field for details. Older Omnipubs may report 110 or 125.
1100 Duplicate Content rejected. Queued for removal. Look for duplicate_type and duplicate_uuid in the response.
310, 320 Bookworm Review, Cashmere Review Waiting for review by Cashmere. Not failed, but it will not advance until a person acts, so stop polling or poll with a timeout. Older Omnipubs may report 105 or 108.
500 Deprecated Terminal. The Omnipub is no longer live.
101 Ready Not Indexed Older Omnipubs only. Processing finished, but the content is not indexed.
Any other code below 200, 410–430, 1000 Processing In progress (410–430 are reprocessing, 1000 is queued). Content is not yet searchable.

Sample Use Cases

Prerequisites:

  • Have a Cashmere account.
  • A Cashmere API key with Read & Write access, from a publisher organization. If you need an API key, see Creating an API Key.

1. Upload via File (Local)

Steps:

  1. Prepare:
    • Optional: Metadata JSON (if not extracting from EPUB).
    • Optional: external_id (to avoid duplicates), tags, collection_ids.
  2. Request (a relative file path is resolved against the current working directory; repeat -F tags=... once per tag):
curl -X POST "https://cashmere.io/api/v2/omnipub" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@/path/to/book.epub" \
  -F "metadata={\"title\":\"Meditations\",\"authors\":[\"Marcus Aurelius\"]}" \
  -F "external_id=9780140455113" \
  -F "tags=metaphysics" \
  -F "tags=philosophy" \
  -F "collection_ids=42"
  1. Response:

    We highly recommend saving the uuid, and pub_hash for lookup later

{
  "status": "success",
  "uuid": "52e24f5a1e0f42f8af09a4a91ded8d16",
  "pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}

2. Upload via URL

Steps:

  1. Prepare:
    • Publicly accessible URL to EPUB/HTML/PDF/Markdown/XML.
    • Optional: Metadata JSON (if not auto-extracted from EPUB format).
  2. Request:
curl -X POST "https://cashmere.io/api/v2/omnipub" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file_url=https://example.com/meditations.epub" \
  -F "metadata={\"title\":\"Meditations\",\"authors\":[\"Marcus Aurelius\"]}" \
  -F "external_id=9780140455113" \
  -F "collection_ids=42"
  1. Response:

    We highly recommend saving the uuid, and pub_hash for lookup later

{
  "status": "success",
  "uuid": "52e24f5a1e0f42f8af09a4a91ded8d16",
  "pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}

3. Upload from Storage Bucket (e.g., S3/GCS/R2)

Steps:

  1. Generate Pre-Signed URL (if private bucket):
# AWS S3 example
import boto3

s3 = boto3.client('s3')
bucket_url = s3.generate_presigned_url(
  'get_object',
  Params={'Bucket': 'your-bucket', 'Key': 'book.epub'},
  ExpiresIn=3600
)
  1. Pass URL to Omnipub endpoint:
curl -X POST "https://cashmere.io/api/v2/omnipub" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file_url=BUCKET_URL" \
  -F "external_id=9780140455113" \
  -F "metadata={\"title\":\"Meditations\",\"authors\":[\"Marcus Aurelius\"]}" \
  -F "collection_ids=42"
  1. Response:

    We highly recommend saving the uuid, and pub_hash for lookup later

{
  "status": "success",
  "uuid": "52e24f5a1e0f42f8af09a4a91ded8d16",
  "pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}

Notes

  • File size: files over 100MB will be rejected.
  • Polling: Do not assume a 202 from the POST endpoint means the content is live. Always poll until the status is indexed (200).
  • Handling 1100: If you receive a 1100 status, use the duplicate_uuid to map your local record to the existing Omnipub.
  • One content source per request: send exactly one of file, file_url, html_content or md_content. A request with more than one is rejected with 400.
  • Web pages: the file type comes from the extension in the file name or URL path, so a file_url with no extension (such as https://example.com/article) is rejected. Send the page's HTML as html_content instead.

Supercharge your API usage

  1. Use consistent external_id patterns for easier tracking - using your systems article ID, or the ISBN allows you to quickly identify content in status checks and reports. This also makes it trivial to query which content has been uploaded vs. what's still pending in your CMS.
  2. Include source_url in all uploads - Even if you're uploading files directly, its a good idea to pass the canonical URL in source_url. This helps users find the original source when they discover your content through AI search, and it's essential for proper attribution.
  3. Provide good metadata upfront - At minimum, we recommend you include title, authors, and external_id. Adding creation_date, publisher, and other metadata improves discoverability and helps with licensing workflows. You can correct metadata later with PUT /api/v2/omnipub/{uuid}/metadata, but changing the content itself means deprecating the Omnipub and uploading it again, so it's best to get it right the first time.