Create an Add-on
Create a single add-on on a publication you own. For batches without images, use the bulk endpoint (up to 100 per request).
Endpoint
POST /api/v3/content/{content}/add-ons
{content} is the publication's internal ID, or any of its identifiers with id_type=external. The publication must be a PDF or an EPUB and must be numbered (a PDF by its finished conversion, an EPUB by its page markers), because every position is validated against the publication's page count. Either condition unmet returns 422 with the reason under errors.content; the overview lists the exact messages.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Max 255 characters |
body | string | Yes | HTML body, sanitized on save. Max 60,000 characters of text content (HTML markup not counted) |
start_page | integer | Yes* | Page where the marker appears, min 1. *Required unless the payload carries an anchor |
anchor | object | No† | Where the reader places the piece. See Anchoring. †Required on EPUB publications |
end_page | integer | No | Last page the piece spans, ≥ the page it starts on. Defaults to that page |
pretitle | string | No | Max 255 characters |
author | string | No | Max 255 characters |
by_line | string | No | Deck or by-line, max 1,000 characters |
type | string | No | article (default), audio, video, image or link |
status | string | No | draft (default) or published |
location_in_start_page | object | No | { "x": 0-100, "y": 0-100 }, marker position as percentages from top-left. Not accepted together with anchor |
image_from_url | string | No | URL of an image to download and attach. See image handling |
image_caption | string | No | Max 2,000 characters |
Both are stamped by the system: source from the creation channel (always integration for this API, and never returned) and body_format by the sanitizer (always html here, returned read-only). Sending either returns 422.
Marker position format
location_in_start_page must be an object with numeric x and y percentages between 0 and 100, measured from the top-left corner of the start page:
{ "x": 12.5, "y": 40 }
Malformed positions (strings, missing coordinates, out-of-range or non-numeric values) are rejected with 422 and a message spelling out the expected format.
Anchoring the piece
A PDF takes either form. An EPUB takes the anchor only: the legacy pair cannot say where a piece sits in a book whose pages are not fixed.
{ "anchor": { "type": "page", "page": 14 } }
On a fixed-layout EPUB, place the piece on a coordinate and name the file of the page it sits on:
{
"anchor": {
"type": "point",
"page": 15,
"locator": {
"href": "OEBPS/page015.xhtml",
"type": "application/xhtml+xml"
},
"point": { "x": 30, "y": 20 }
}
}
A point anchor needs a page whose geometry does not reflow, so on a reflowable EPUB it is refused with A point anchor needs a page with fixed geometry; a reflowable EPUB takes a page anchor. under errors.anchor.type. A page beyond the publication is refused under errors.anchor.page, the same way start_page is.
Image handling
Images are referenced by URL only. There is no direct file upload. On create, the system:
- Downloads the image from
image_from_url(15-second timeout). - Validates it is a real JPEG, PNG, GIF or WebP under 10 MB. Anything else is rejected with
422and the add-on is not created. - Optimizes it (fitted within 1024×1024, quality 85) and stores it on the publication's asset storage.
The stored copy's URL is returned as image_url. image_url itself is a response field: sending it in a request returns 422.
Request Example
On a PDF, with a marker on the page:
curl -X POST "https://yourstore.publica.la/api/v3/content/123/add-ons" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Interview with the author",
"body": "<p>Full interview…</p>",
"start_page": 12,
"location_in_start_page": { "x": 25.5, "y": 40 },
"image_from_url": "https://example.com/photo.jpg",
"status": "published"
}'
On an EPUB, anchored to the page the reader shows as 14:
curl -X POST "https://yourstore.publica.la/api/v3/content/456/add-ons" \
-H "X-User-Token: your-api-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Context for chapter three",
"body": "<p>Full piece…</p>",
"anchor": { "type": "page", "page": 14 },
"status": "published"
}'
Response
201 Created with the add-on object under data, plus a sanitization object listing what the body sanitizer removed (empty when nothing was).
{
"data": {
"id": "1044",
"content_id": "123",
"title": "Interview with the author",
"body": "<p>Full interview…</p>",
"body_format": "html",
"status": "published",
"start_page": 12,
"end_page": 12,
"location_in_start_page": { "x": 25.5, "y": 40 },
"anchor": { "type": "point", "page": 12, "point": { "x": 25.5, "y": 40 } },
"image_url": "https://assets.publica.la/yourstore/…/articles/article_1755100000000.jpg",
"created_at": "2026-08-13T14:05:00.000000Z",
"updated_at": "2026-08-13T14:05:00.000000Z"
},
"sanitization": {
"discarded": []
}
}
Compare data.body with what you sent when sanitization.discarded is not empty: that is the HTML readers will see.
Errors
| Status | Condition |
|---|---|
401 | Missing or invalid X-User-Token |
403 | Add-ons feature not enabled, non-administrator token, or the publication is not owned by your store |
404 | Publication not found |
422 | Validation failure: missing required field, no position at all, both forms of position at once, an incomplete or out-of-range anchor, a point anchor on a reflowable EPUB, an EPUB create without an anchor, malformed marker position, source, body_format or image_url provided, invalid status, a body that sanitizes to nothing, image URL not downloadable or not a supported image, page bounds beyond the publication, or a publication that takes no add-ons or is not numbered yet |
429 | Rate limit exceeded (20 requests per minute for create and update, since each may fetch a remote image) |