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
Authorizationheader 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
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. |
| 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_idfield when adding Omnipubs so that it is easier to identify your data in our database.Examples of an
external_idare an ISBN, blog post id, or UUID that you use to identify content on your system.We also strongly recommend saving the
pub_hashanduuidfrom 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:
- Prepare:
- Optional: Metadata JSON (if not extracting from EPUB).
- Optional: external_id (to avoid duplicates), tags, collection_ids.
- 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"
-
Response:
We highly recommend saving the
uuid, andpub_hashfor lookup later
{
"status": "success",
"uuid": "52e24f5a1e0f42f8af09a4a91ded8d16",
"pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}
2. Upload via URL
Steps:
- Prepare:
- Publicly accessible URL to EPUB/HTML/PDF/Markdown/XML.
- Optional: Metadata JSON (if not auto-extracted from EPUB format).
- 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"
-
Response:
We highly recommend saving the
uuid, andpub_hashfor lookup later
{
"status": "success",
"uuid": "52e24f5a1e0f42f8af09a4a91ded8d16",
"pub_hash": "ae803b4d136ce2f6a80d64d15ad3dfc7"
}
3. Upload from Storage Bucket (e.g., S3/GCS/R2)
Steps:
- 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
)
- 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"
-
Response:
We highly recommend saving the
uuid, andpub_hashfor 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_contentormd_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_urlwith no extension (such ashttps://example.com/article) is rejected. Send the page's HTML ashtml_contentinstead.
Supercharge your API usage
- 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.
- 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.
- 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.