Product Files v4
Sub-resource of products holding the binary assets that belong to a product: the author's source manuscript, the e-book files distributors receive, the print-ready PDFs a print-on-demand supplier needs, the marketing cover image, the reading extract and the HTML preview of the opening pages. One product can carry many of them.
Each record describes one asset. What the asset is for is carried by its type; the
fields that describe the bytes — file name, size, MIME type, SHA-1, image dimensions,
EPUB version — are derived by the server rather than supplied by the client. A record
may instead register a file hosted elsewhere, in which case it holds a reference and the
platform never receives the bytes at all.
Endpoints
GET /resource/v4/products/{product_id}/files
Retrieve the files of a product
Description
Returns all file records of the product, paginated (100 per page by default).
Exactly six fields are filterable — type, labels, mime_type, file_name,
property_epub_version and channel — and any other query parameter that is not a
pagination or sorting parameter is rejected with 400 Bad Request. The scalar
operators apply on all six except labels, which accepts equality only; see
Resource Queries.
Two behaviours are easy to trip over: labels takes a single exact label and
returns nothing for a comma-separated list, and channel needs the literal value
NULL to select channel-independent files.
…/assets is a deprecated alias for …/files. It returns the same records, but the
download and validation sub-paths are not available under it. New integrations
should address …/files.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No | UUID (or legacy integer id) of the product. |
Query parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
channel |
string | No | Filter by distribution channel, e.g. `?channel=google`. The literal `?channel=NULL` is special-cased to select files that are *not* restricted to a channel — the usual case. | |
file_name |
string | No | Filter by the uploaded file name. An operator gives partial matches, e.g. `?file_name__endswith=.epub`. | |
labels |
string | No | Filter by a single label, matched exactly — `?labels=auto` does not match `autogenerated`, and equality is the only operator accepted (`?labels__contains` is a `400 Bad Request`). A comma-separated list is accepted but always returns an empty list, even for a file that carries every label named; one request per label, intersected client-side, is the way to express "and". | |
mime_type |
string | No | Filter by media type, e.g. `?mime_type=application/epub+zip`. Records that carry no media type at all — URL registrations and older records — are selected with `?mime_type__isnull`. | |
property_epub_version |
string | No | Filter EPUBs by version: `2` or `3`. Any other value is rejected with `400 Bad Request`. Because detection falls back to EPUB 2 whenever EPUB 3 was not positively identified, `?property_epub_version=2` also returns files that were merely not recognised as EPUB 3; files for which no version was detected at all are returned by neither value. | |
sort |
string | No | Sort order. Two field names are defined for this resource and are the two to rely on: `id` and `timestamp`. Other names may or may not resolve, and one that does not is rejected with `400 Bad Request`. The two defined orders do not always agree, because old records carry the timestamp of the original import rather than of the date the record was added here. For the syntax see [Resource Queries](/resource_queries/). | |
type |
string | No | Filter by file type, e.g. `?type=ebook_epub` to fetch only the EPUB editions. An unknown type is rejected with `400 Bad Request` rather than returning an empty result. `?type=audiobook` matches nothing, because no record ever carries that type — see `type` in the Product File schema below. |
Responses
{
"_links": {},
"_pagination": {},
"items": [
{
"_id": "46033f29-9733-45fa-8069-7e7a23dbe864",
"_legacy_id": 20882910,
"_version": "4.0",
"_links": {},
"type": "cover_marketing",
"timestamp": 1782319836,
"file_size": 267184,
"file_name": "v14937.jpg",
"mime_type": "image/jpeg",
"labels": [
"autogenerated"
],
"properties": {
"height": 2560,
"width": 1809
},
"validation": {
"result": "failed",
"errors": [
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
],
"warnings": []
},
"sha1": "9f4806983de39ef7b503c4c658d37427d7caf5f4",
"channel": null,
"href": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"properties": {
"_links": {
"type": "object"
},
"_pagination": {
"type": "object"
},
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"description": "One binary asset attached to a product, identified by its file type.",
"properties": {
"_id": {
"type": "string",
"description": "UUID of the file record. Stable for the lifetime of the record; a replaced file gets a new one.",
"example": "46033f29-9733-45fa-8069-7e7a23dbe864"
},
"_legacy_id": {
"type": "integer",
"description": "Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. This is the id the v3 API returns as `_id`, and it is accepted in place of the UUID anywhere in the path.",
"example": 20882910
},
"_version": {
"type": "string",
"description": "API version that produced this representation.",
"example": "4.0"
},
"_links": {
"type": "object",
"description": "`self` — this file record. `tests` — the collection of validation checks run\nagainst it (`/resource/v4/products/{product_id}/files/{file_id}/tests`), which is\nthe itemised form of the summary in `validation`.\n"
},
"type": {
"type": "string",
"description": "What the file is for — a manuscript, an e-book edition, a cover image, a preview.\nThis is the only field that decides how the platform treats the asset, and the only\none that is mandatory on upload; neither the file name nor the MIME type carries that\nmeaning. Twenty values are accepted; anything else is rejected with\n`400 Bad Request`.\n\n**A file record is immutable.** There is no `PUT`. An asset is replaced by a `DELETE`\nof the record followed by a `POST` of the new file under the same `type`. Apart from\n`channel` and `labels`, every remaining field is derived by the server from the\nuploaded bytes, so there is nothing else a client could update.\n\nSource material:\n\n- `dropbox` — author-supplied source manuscript: the document a publication is\n produced *from*, not a deliverable. Unrelated to the Dropbox service.\n\nE-book editions:\n\n- `ebook_epub` — the EPUB edition. Inspected on upload: the version lands in\n `properties.epub_version` and structural features in `labels`.\n- `ebook_pdf` — the PDF edition.\n- `ebook_pdf_cover` — cover page bound into the PDF edition.\n- `ebook_pdf_bastard_title` — half-title (bastard title) page bound into the PDF\n edition.\n- `ebook_mobi` — the Mobipocket / Kindle edition.\n- `ebook_ibook` — the Apple iBooks edition.\n\nPrint-on-demand:\n\n- `pod_bookblock` — the book block, i.e. the interior pages, as a print-ready PDF.\n- `pod_cover` — the cover as one wrap: front, spine and back together.\n- `pod_cover_front` — front cover only.\n- `pod_cover_back` — back cover only.\n\nCovers, previews and samples:\n\n- `cover_marketing` — the marketing cover image. **Must be a JPEG**: the file name\n has to end in `.jpg` or `.jpeg`, or the upload is rejected with `400 Bad Request`.\n The test is on the extension alone, so a PNG renamed to `.jpg` passes it.\n- `extract` — a reading extract offered as a sample of the work.\n- `shop_preview` — HTML preview of the opening pages.\n- `shop_preview_pdf` — a preview offered as a downloadable PDF.\n- `screenshot` — screenshot of the product, for app-like or interactive titles.\n\nAudiobooks:\n\n- `audiobook` — an instruction to unpack a complete audiobook delivery. No record ever\n carries it: an upload with `type=audiobook` is unpacked and each entry becomes a\n record of one of the three types below, chosen from the entry's own media type —\n `audio/*` becomes a track, `image/*` the cover, `application/*` the booklet. Only a\n delivery that unpacks to exactly one record can be answered with that record, so an\n archive of several tracks is stored and then reported as `400 Bad Request`; the\n derived records are visible on the next `GET`. Re-uploading replaces the previously\n derived set.\n- `audiobook_track` — a single audio track of an audiobook.\n- `audiobook_booklet` — the booklet accompanying an audiobook.\n- `audiobook_cover` — cover artwork for an audiobook.\n\nOnly the twenty values listed above are accepted on input, and only they are ever\nreturned.\n\nFilterable — `?type=ebook_epub`; an unknown type is rejected with `400 Bad Request`\nrather than returning an empty result. See\n[Resource Queries](/resource_queries/).\n\n`null` on legacy records that carry no type at all. A legacy record whose type is\nneither absent nor one of the twenty cannot be represented and answers\n`400 Bad Request` when read.\n",
"nullable": true,
"example": "cover_marketing",
"x-enum-descriptions": {
"pod_bookblock": "Print-on-demand book block — the interior pages, print-ready PDF.",
"pod_cover": "Print-on-demand cover — the complete wrap (front, spine, back) as one print-ready PDF.",
"pod_cover_front": "Print-on-demand front cover only.",
"pod_cover_back": "Print-on-demand back cover only.",
"ebook_pdf": "The PDF edition of the e-book, as delivered to customers and channels.",
"ebook_pdf_cover": "Cover page bound into the PDF e-book.",
"ebook_pdf_bastard_title": "Half-title (bastard title) page bound into the PDF e-book.",
"ebook_epub": "The EPUB edition. Automatically inspected on upload: EPUB version lands in `properties.epub_version` and structural features in `labels`.",
"ebook_mobi": "The Mobipocket/Kindle edition.",
"ebook_ibook": "The Apple iBooks edition.",
"cover_marketing": "The marketing cover image. Must be a JPEG — any other format is rejected with `400 Bad Request`.",
"extract": "A reading extract offered as a sample of the work.",
"shop_preview": "HTML preview rendered inside the storefront product page.",
"shop_preview_pdf": "PDF preview offered for download from the storefront.",
"screenshot": "Screenshot of the product, e.g. for app-like or interactive titles.",
"dropbox": "Author-supplied source manuscript. The original document a publication is produced from, not a deliverable.",
"audiobook": "An instruction to unpack a complete audiobook delivery, normally a ZIP. Never carried by a record: each entry becomes an `audiobook_track`, `audiobook_cover` or `audiobook_booklet` record.",
"audiobook_track": "A single audio track of an audiobook.",
"audiobook_booklet": "The booklet accompanying an audiobook.",
"audiobook_cover": "Cover artwork for an audiobook."
}
},
"timestamp": {
"type": "integer",
"description": "When the file was added, as a Unix timestamp (seconds since 1970-01-01 UTC, integer,\nnot a string).\n\nSortable. `id` and `timestamp` are the two field names defined for this resource and\nthe two to rely on; other names may or may not resolve, and one that does not is\nrejected with `400 Bad Request`. For the syntax see\n[Resource Queries](/resource_queries/).\n\nOld records carry the timestamp of the original import, which can predate the platform\nitself, so the two defined orders do not always agree: the oldest file on a\nlong-running title can be decades older than its `_id` ordering suggests.\n",
"example": 1782319836
},
"file_size": {
"type": "integer",
"description": "Size of the stored file in bytes. `null` for records registered by URL, where the\nplatform never receives the bytes.\n\nThere is an upload size limit, and exceeding it answers `413` rather than\n`400 Bad Request`. For a file of 4 GiB or more the value reported here is not the true\nbyte count, even though the file itself is stored whole.\n",
"nullable": true,
"example": 267184
},
"file_name": {
"type": "string",
"description": "The name the file was uploaded under, with any directory part stripped. Informational\nonly — the platform never dispatches on it, and duplicate names within one product are\nallowed. It is echoed back in the `Content-Disposition` header when the content is\ndownloaded, so it is what a browser will save the file as.\n",
"nullable": true,
"example": "v14937.jpg"
},
"mime_type": {
"type": "string",
"description": "Media type of the file. Guessed by the server from the file name extension at\nupload time — the `Content-Type` of the multipart part is ignored, and so is the\ncontent of the file.\n\nThere is no allow-list: an extension the server does not recognise is recorded as\n`application/octet-stream` rather than rejected, so this field never reports failure.\n\n`null` for records registered by URL, whose bytes the platform never receives, and for\nolder records created before media types were recorded. Both are selectable with\n`?mime_type__isnull`.\n\nThis drives which extras appear in `properties`: `image/*` yields `height` and\n`width`, `application/epub+zip` yields `epub_version`.\n\nFilterable — `?mime_type=application/epub+zip`. See\n[Resource Queries](/resource_queries/).\n",
"nullable": true,
"example": "image/jpeg"
},
"labels": {
"type": "array",
"description": "Free-form lowercase tags on the file. Always present, and empty when the file has no\ntags. Client-supplied tags are passed on upload as a comma-separated `labels`\nparameter; they are lowercased and trimmed, and there is no allow-list. They are only\napplied to an uploaded file: a `labels` parameter sent alongside `url` is accepted and\nsilently dropped, leaving the registered record untagged.\n\nTwo families of label are set by the platform itself. The first records how the file\ncame to be:\n\n- `autogenerated` — the file was produced by the platform's own conversion pipeline\n rather than uploaded by a human. Replacing an `autogenerated` file by hand is\n usually futile, because the next conversion run overwrites it again.\n- `book-converter` — names the pipeline that produced the file.\n\nThe second describes the structure of an EPUB. Uploading an `ebook_epub` makes the\nserver open the book and tag it with what it finds:\n\n- `fixedlayout` — fixed-layout rather than reflowable\n- `svgimage` — images supplied as SVG\n- `svg` — SVG content\n- `video` — embedded video\n- `audio` — embedded audio\n- `object` — embedded objects\n\nFilterable by one exact label at a time, equality only: `?labels=autogenerated`\nworks, `?labels=auto` matches nothing, and `?labels__contains=auto` is a\n`400 Bad Request`. A comma-separated list is accepted in a *filter* but always returns\nan empty list, even for a file carrying every label named. One request per label,\nintersected client-side, is the way to express \"and\". See\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"autogenerated"
]
},
"properties": {
"type": "object",
"additionalProperties": true,
"description": "Format-specific attributes the server extracted from the file. The keys depend on\n`mime_type` and are absent — the object is then `{}` — for formats that carry none:\n\n- `height`, `width` — pixel dimensions, **integers**, for any `image/*` file. They\n are the only record of the resolution of a `cover_marketing` image.\n- `epub_version` — `\"2\"` or `\"3\"`, a **string**, for `application/epub+zip`. Determined\n at upload time, and it defaults to `\"2\"` whenever the server did not positively\n identify an EPUB 3, so `\"2\"` means \"not detected as EPUB 3\" rather than \"confirmed\n EPUB 2\".\n\nOne key is filterable: `?property_epub_version=3`. Only `2` and `3` are accepted\nthere; any other value is a `400 Bad Request`. The filter matches the version the\nserver detected, so a record for which no version was ever detected is returned by\nneither `2` nor `3`.\n",
"example": {
"height": 2560,
"width": 1809
}
},
"validation": {
"type": "object",
"additionalProperties": false,
"description": "Rolled-up outcome of every validation check run against this file, or `null` when no\ncheck has ever run — which is the normal state for formats nobody validates, such as\ncover images and HTML previews. `null` therefore means *unknown*, not *passed*.\n\nChecks are not run by the upload request. A file is created with `validation: null`\nand acquires a result seconds to minutes later, once a checker has looked at it, so a\nread-back immediately after an upload still shows `null`. The checkers whose verdicts\nappear here are `pdf-check` for PDFs and `epubchecker` for EPUBs; the per-checker\ndetail behind this summary is at `_links.tests`.\n",
"nullable": true,
"properties": {
"result": {
"type": "string",
"description": "`passed` if every check on the file passed, `failed` if any one of them failed.\nA `failed` file is still stored and still downloadable: this resource reports the\nverdict, it does not act on it.\n",
"example": "failed"
},
"errors": {
"type": "array",
"description": "Every error message from every check, flattened into one list. For EPUBs these\nare EPUBCheck messages and keep their original identifier and location prefix,\nwhich is what identifies the fault inside the source file.\n",
"items": {
"type": "string"
},
"example": [
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
]
},
"warnings": {
"type": "array",
"description": "Every warning message from every check, flattened into one list. Warnings do not make `result` `failed`.",
"items": {
"type": "string"
},
"example": []
}
}
},
"sha1": {
"type": "string",
"description": "SHA-1 digest of the stored bytes, lowercase hex. It is what confirms that a download\nmatches what was stored, and — since file names may repeat and a replaced file keeps\nneither its `_id` nor necessarily a new name — what identifies the bytes themselves.\n`null` for files stored before hashing was introduced, and for URL-registered records\nunless a `sha1` was supplied at creation, in which case the supplied value is returned\nunverified.\n",
"nullable": true,
"example": "9f4806983de39ef7b503c4c658d37427d7caf5f4"
},
"channel": {
"type": "string",
"description": "Restricts the file to one distribution channel, identified by the same lowercase\nchannel slug used elsewhere in the API (`google`, `itunes`, …). It exists for the case\nwhere one channel needs its own variant of an asset — a differently sized cover, a\nwatermarked extract — that must not reach the others.\n\n`null`, the usual case, means the file is not restricted to a channel. Filtering for\nthose records needs the literal, case-sensitive `?channel=NULL`; a plain `?channel=`\ndoes not match them.\n",
"nullable": true,
"example": null
},
"href": {
"type": "string",
"description": "Where the bytes are. For a stored file this is the download endpoint of this API\n(`.../files/{file_id}/file`), and requires the same `Authorization` header as any\nother call — it is not a public or pre-signed link, and it does not expire. For a\nrecord created with `?url=` it is the external URL exactly as registered, which is\nneither fetched nor validated.\n",
"example": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
},
"required": []
}
}
},
"required": [
"_links",
"_pagination",
"items"
]
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
POST /resource/v4/products/{product_id}/files
Upload a file for a product
Description
The bytes travel as a multipart/form-data part named file; all the metadata is
passed as query parameters on the same request, not as form fields:
POST /resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files
?type=ebook_epub&labels=distribution:google
Content-Type: multipart/form-data; boundary=...
--...
Content-Disposition: form-data; name="file"; filename="v14937-final.epub"
<bytes>
An externally hosted file is registered by omitting the body and passing url
instead.
Only type is mandatory. There is no MIME-type allow-list: an unrecognised
extension is accepted and recorded as application/octet-stream, and nothing checks
that the bytes are the kind of file the type claims. There is an upload size limit,
and exceeding it answers 413.
The response is the complete file record, including the fields the server derived
from the bytes. validation is null in it even for formats that are checked,
because checking happens afterwards.
type=audiobook behaves differently from every other value: it unpacks the archive
into one record per entry, so it can only answer 201 when the archive holds a
single entry. A multi-track archive is stored and then reported as
400 Bad Request, with the derived records visible on the next GET.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No | UUID (or legacy integer id) of the product. |
Query parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
channel |
string | No | Restrict the file to one distribution channel. Left out — the usual case — the file is available to all channels. | |
labels |
string | No | Comma-separated tags to attach to the file. Lowercased and trimmed; empty entries are dropped. Any string is accepted. Applied only to an uploaded file — sent together with `url` it is accepted and silently ignored. | |
md5 |
string | No | MD5 digest of the external file. Only meaningful together with `url`; recorded as supplied and not verified. | |
sha1 |
string | No | SHA-1 digest of the external file. Only meaningful together with `url`; recorded as supplied and not verified, and returned as the record's `sha1`. | |
type |
string | No | What the file is for. Mandatory. The twenty accepted values, and what each one means, are listed under `type` in the Product File schema below; an unknown value is rejected with `400 Bad Request`. | |
url |
string | No | Register a file hosted elsewhere rather than uploading one. Mutually exclusive with the `file` part; if both are present the uploaded part wins. The URL is recorded as supplied and is neither fetched nor checked. |
Request body
{
"file": "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="
}
Schema of the request body
{
"type": "object",
"additionalProperties": false,
"properties": {
"file": {
"type": "string",
"format": "binary",
"description": "The file itself. The part's `filename` becomes the record's `file_name` and its extension decides the reported `mime_type`; the part's own `Content-Type` is ignored. The whole body is left out when registering a `url` instead."
}
},
"required": [
"file"
]
}
Responses
{
"_id": "46033f29-9733-45fa-8069-7e7a23dbe864",
"_legacy_id": 20882910,
"_version": "4.0",
"_links": {},
"type": "cover_marketing",
"timestamp": 1782319836,
"file_size": 267184,
"file_name": "v14937.jpg",
"mime_type": "image/jpeg",
"labels": [
"autogenerated"
],
"properties": {
"height": 2560,
"width": 1809
},
"validation": {
"result": "failed",
"errors": [
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
],
"warnings": []
},
"sha1": "9f4806983de39ef7b503c4c658d37427d7caf5f4",
"channel": null,
"href": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One binary asset attached to a product, identified by its file type.",
"properties": {
"_id": {
"type": "string",
"description": "UUID of the file record. Stable for the lifetime of the record; a replaced file gets a new one.",
"example": "46033f29-9733-45fa-8069-7e7a23dbe864"
},
"_legacy_id": {
"type": "integer",
"description": "Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. This is the id the v3 API returns as `_id`, and it is accepted in place of the UUID anywhere in the path.",
"example": 20882910
},
"_version": {
"type": "string",
"description": "API version that produced this representation.",
"example": "4.0"
},
"_links": {
"type": "object",
"description": "`self` — this file record. `tests` — the collection of validation checks run\nagainst it (`/resource/v4/products/{product_id}/files/{file_id}/tests`), which is\nthe itemised form of the summary in `validation`.\n"
},
"type": {
"type": "string",
"description": "What the file is for — a manuscript, an e-book edition, a cover image, a preview.\nThis is the only field that decides how the platform treats the asset, and the only\none that is mandatory on upload; neither the file name nor the MIME type carries that\nmeaning. Twenty values are accepted; anything else is rejected with\n`400 Bad Request`.\n\n**A file record is immutable.** There is no `PUT`. An asset is replaced by a `DELETE`\nof the record followed by a `POST` of the new file under the same `type`. Apart from\n`channel` and `labels`, every remaining field is derived by the server from the\nuploaded bytes, so there is nothing else a client could update.\n\nSource material:\n\n- `dropbox` — author-supplied source manuscript: the document a publication is\n produced *from*, not a deliverable. Unrelated to the Dropbox service.\n\nE-book editions:\n\n- `ebook_epub` — the EPUB edition. Inspected on upload: the version lands in\n `properties.epub_version` and structural features in `labels`.\n- `ebook_pdf` — the PDF edition.\n- `ebook_pdf_cover` — cover page bound into the PDF edition.\n- `ebook_pdf_bastard_title` — half-title (bastard title) page bound into the PDF\n edition.\n- `ebook_mobi` — the Mobipocket / Kindle edition.\n- `ebook_ibook` — the Apple iBooks edition.\n\nPrint-on-demand:\n\n- `pod_bookblock` — the book block, i.e. the interior pages, as a print-ready PDF.\n- `pod_cover` — the cover as one wrap: front, spine and back together.\n- `pod_cover_front` — front cover only.\n- `pod_cover_back` — back cover only.\n\nCovers, previews and samples:\n\n- `cover_marketing` — the marketing cover image. **Must be a JPEG**: the file name\n has to end in `.jpg` or `.jpeg`, or the upload is rejected with `400 Bad Request`.\n The test is on the extension alone, so a PNG renamed to `.jpg` passes it.\n- `extract` — a reading extract offered as a sample of the work.\n- `shop_preview` — HTML preview of the opening pages.\n- `shop_preview_pdf` — a preview offered as a downloadable PDF.\n- `screenshot` — screenshot of the product, for app-like or interactive titles.\n\nAudiobooks:\n\n- `audiobook` — an instruction to unpack a complete audiobook delivery. No record ever\n carries it: an upload with `type=audiobook` is unpacked and each entry becomes a\n record of one of the three types below, chosen from the entry's own media type —\n `audio/*` becomes a track, `image/*` the cover, `application/*` the booklet. Only a\n delivery that unpacks to exactly one record can be answered with that record, so an\n archive of several tracks is stored and then reported as `400 Bad Request`; the\n derived records are visible on the next `GET`. Re-uploading replaces the previously\n derived set.\n- `audiobook_track` — a single audio track of an audiobook.\n- `audiobook_booklet` — the booklet accompanying an audiobook.\n- `audiobook_cover` — cover artwork for an audiobook.\n\nOnly the twenty values listed above are accepted on input, and only they are ever\nreturned.\n\nFilterable — `?type=ebook_epub`; an unknown type is rejected with `400 Bad Request`\nrather than returning an empty result. See\n[Resource Queries](/resource_queries/).\n\n`null` on legacy records that carry no type at all. A legacy record whose type is\nneither absent nor one of the twenty cannot be represented and answers\n`400 Bad Request` when read.\n",
"nullable": true,
"example": "cover_marketing",
"x-enum-descriptions": {
"pod_bookblock": "Print-on-demand book block — the interior pages, print-ready PDF.",
"pod_cover": "Print-on-demand cover — the complete wrap (front, spine, back) as one print-ready PDF.",
"pod_cover_front": "Print-on-demand front cover only.",
"pod_cover_back": "Print-on-demand back cover only.",
"ebook_pdf": "The PDF edition of the e-book, as delivered to customers and channels.",
"ebook_pdf_cover": "Cover page bound into the PDF e-book.",
"ebook_pdf_bastard_title": "Half-title (bastard title) page bound into the PDF e-book.",
"ebook_epub": "The EPUB edition. Automatically inspected on upload: EPUB version lands in `properties.epub_version` and structural features in `labels`.",
"ebook_mobi": "The Mobipocket/Kindle edition.",
"ebook_ibook": "The Apple iBooks edition.",
"cover_marketing": "The marketing cover image. Must be a JPEG — any other format is rejected with `400 Bad Request`.",
"extract": "A reading extract offered as a sample of the work.",
"shop_preview": "HTML preview rendered inside the storefront product page.",
"shop_preview_pdf": "PDF preview offered for download from the storefront.",
"screenshot": "Screenshot of the product, e.g. for app-like or interactive titles.",
"dropbox": "Author-supplied source manuscript. The original document a publication is produced from, not a deliverable.",
"audiobook": "An instruction to unpack a complete audiobook delivery, normally a ZIP. Never carried by a record: each entry becomes an `audiobook_track`, `audiobook_cover` or `audiobook_booklet` record.",
"audiobook_track": "A single audio track of an audiobook.",
"audiobook_booklet": "The booklet accompanying an audiobook.",
"audiobook_cover": "Cover artwork for an audiobook."
}
},
"timestamp": {
"type": "integer",
"description": "When the file was added, as a Unix timestamp (seconds since 1970-01-01 UTC, integer,\nnot a string).\n\nSortable. `id` and `timestamp` are the two field names defined for this resource and\nthe two to rely on; other names may or may not resolve, and one that does not is\nrejected with `400 Bad Request`. For the syntax see\n[Resource Queries](/resource_queries/).\n\nOld records carry the timestamp of the original import, which can predate the platform\nitself, so the two defined orders do not always agree: the oldest file on a\nlong-running title can be decades older than its `_id` ordering suggests.\n",
"example": 1782319836
},
"file_size": {
"type": "integer",
"description": "Size of the stored file in bytes. `null` for records registered by URL, where the\nplatform never receives the bytes.\n\nThere is an upload size limit, and exceeding it answers `413` rather than\n`400 Bad Request`. For a file of 4 GiB or more the value reported here is not the true\nbyte count, even though the file itself is stored whole.\n",
"nullable": true,
"example": 267184
},
"file_name": {
"type": "string",
"description": "The name the file was uploaded under, with any directory part stripped. Informational\nonly — the platform never dispatches on it, and duplicate names within one product are\nallowed. It is echoed back in the `Content-Disposition` header when the content is\ndownloaded, so it is what a browser will save the file as.\n",
"nullable": true,
"example": "v14937.jpg"
},
"mime_type": {
"type": "string",
"description": "Media type of the file. Guessed by the server from the file name extension at\nupload time — the `Content-Type` of the multipart part is ignored, and so is the\ncontent of the file.\n\nThere is no allow-list: an extension the server does not recognise is recorded as\n`application/octet-stream` rather than rejected, so this field never reports failure.\n\n`null` for records registered by URL, whose bytes the platform never receives, and for\nolder records created before media types were recorded. Both are selectable with\n`?mime_type__isnull`.\n\nThis drives which extras appear in `properties`: `image/*` yields `height` and\n`width`, `application/epub+zip` yields `epub_version`.\n\nFilterable — `?mime_type=application/epub+zip`. See\n[Resource Queries](/resource_queries/).\n",
"nullable": true,
"example": "image/jpeg"
},
"labels": {
"type": "array",
"description": "Free-form lowercase tags on the file. Always present, and empty when the file has no\ntags. Client-supplied tags are passed on upload as a comma-separated `labels`\nparameter; they are lowercased and trimmed, and there is no allow-list. They are only\napplied to an uploaded file: a `labels` parameter sent alongside `url` is accepted and\nsilently dropped, leaving the registered record untagged.\n\nTwo families of label are set by the platform itself. The first records how the file\ncame to be:\n\n- `autogenerated` — the file was produced by the platform's own conversion pipeline\n rather than uploaded by a human. Replacing an `autogenerated` file by hand is\n usually futile, because the next conversion run overwrites it again.\n- `book-converter` — names the pipeline that produced the file.\n\nThe second describes the structure of an EPUB. Uploading an `ebook_epub` makes the\nserver open the book and tag it with what it finds:\n\n- `fixedlayout` — fixed-layout rather than reflowable\n- `svgimage` — images supplied as SVG\n- `svg` — SVG content\n- `video` — embedded video\n- `audio` — embedded audio\n- `object` — embedded objects\n\nFilterable by one exact label at a time, equality only: `?labels=autogenerated`\nworks, `?labels=auto` matches nothing, and `?labels__contains=auto` is a\n`400 Bad Request`. A comma-separated list is accepted in a *filter* but always returns\nan empty list, even for a file carrying every label named. One request per label,\nintersected client-side, is the way to express \"and\". See\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"autogenerated"
]
},
"properties": {
"type": "object",
"additionalProperties": true,
"description": "Format-specific attributes the server extracted from the file. The keys depend on\n`mime_type` and are absent — the object is then `{}` — for formats that carry none:\n\n- `height`, `width` — pixel dimensions, **integers**, for any `image/*` file. They\n are the only record of the resolution of a `cover_marketing` image.\n- `epub_version` — `\"2\"` or `\"3\"`, a **string**, for `application/epub+zip`. Determined\n at upload time, and it defaults to `\"2\"` whenever the server did not positively\n identify an EPUB 3, so `\"2\"` means \"not detected as EPUB 3\" rather than \"confirmed\n EPUB 2\".\n\nOne key is filterable: `?property_epub_version=3`. Only `2` and `3` are accepted\nthere; any other value is a `400 Bad Request`. The filter matches the version the\nserver detected, so a record for which no version was ever detected is returned by\nneither `2` nor `3`.\n",
"example": {
"height": 2560,
"width": 1809
}
},
"validation": {
"type": "object",
"additionalProperties": false,
"description": "Rolled-up outcome of every validation check run against this file, or `null` when no\ncheck has ever run — which is the normal state for formats nobody validates, such as\ncover images and HTML previews. `null` therefore means *unknown*, not *passed*.\n\nChecks are not run by the upload request. A file is created with `validation: null`\nand acquires a result seconds to minutes later, once a checker has looked at it, so a\nread-back immediately after an upload still shows `null`. The checkers whose verdicts\nappear here are `pdf-check` for PDFs and `epubchecker` for EPUBs; the per-checker\ndetail behind this summary is at `_links.tests`.\n",
"nullable": true,
"properties": {
"result": {
"type": "string",
"description": "`passed` if every check on the file passed, `failed` if any one of them failed.\nA `failed` file is still stored and still downloadable: this resource reports the\nverdict, it does not act on it.\n",
"example": "failed"
},
"errors": {
"type": "array",
"description": "Every error message from every check, flattened into one list. For EPUBs these\nare EPUBCheck messages and keep their original identifier and location prefix,\nwhich is what identifies the fault inside the source file.\n",
"items": {
"type": "string"
},
"example": [
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
]
},
"warnings": {
"type": "array",
"description": "Every warning message from every check, flattened into one list. Warnings do not make `result` `failed`.",
"items": {
"type": "string"
},
"example": []
}
}
},
"sha1": {
"type": "string",
"description": "SHA-1 digest of the stored bytes, lowercase hex. It is what confirms that a download\nmatches what was stored, and — since file names may repeat and a replaced file keeps\nneither its `_id` nor necessarily a new name — what identifies the bytes themselves.\n`null` for files stored before hashing was introduced, and for URL-registered records\nunless a `sha1` was supplied at creation, in which case the supplied value is returned\nunverified.\n",
"nullable": true,
"example": "9f4806983de39ef7b503c4c658d37427d7caf5f4"
},
"channel": {
"type": "string",
"description": "Restricts the file to one distribution channel, identified by the same lowercase\nchannel slug used elsewhere in the API (`google`, `itunes`, …). It exists for the case\nwhere one channel needs its own variant of an asset — a differently sized cover, a\nwatermarked extract — that must not reach the others.\n\n`null`, the usual case, means the file is not restricted to a channel. Filtering for\nthose records needs the literal, case-sensitive `?channel=NULL`; a plain `?channel=`\ndoes not match them.\n",
"nullable": true,
"example": null
},
"href": {
"type": "string",
"description": "Where the bytes are. For a stored file this is the download endpoint of this API\n(`.../files/{file_id}/file`), and requires the same `Authorization` header as any\nother call — it is not a public or pre-signed link, and it does not expire. For a\nrecord created with `?url=` it is the external URL exactly as registered, which is\nneither fetched nor validated.\n",
"example": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
},
"required": []
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
GET /resource/v4/products/{product_id}/files/{file_id}
Retrieve a single product file
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
file_id |
string | No | UUID (or legacy integer id) of the file. | |
product_id |
string | No | UUID (or legacy integer id) of the product. |
Responses
{
"_id": "46033f29-9733-45fa-8069-7e7a23dbe864",
"_legacy_id": 20882910,
"_version": "4.0",
"type": "cover_marketing",
"file_name": "v14937.jpg",
"file_size": 267184,
"mime_type": "image/jpeg",
"properties": {
"height": 2560,
"width": 1809
},
"labels": [
"autogenerated"
],
"sha1": "9f4806983de39ef7b503c4c658d37427d7caf5f4",
"channel": null,
"timestamp": 1782319836,
"validation": null,
"href": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One binary asset attached to a product, identified by its file type.",
"properties": {
"_id": {
"type": "string",
"description": "UUID of the file record. Stable for the lifetime of the record; a replaced file gets a new one.",
"example": "46033f29-9733-45fa-8069-7e7a23dbe864"
},
"_legacy_id": {
"type": "integer",
"description": "Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. This is the id the v3 API returns as `_id`, and it is accepted in place of the UUID anywhere in the path.",
"example": 20882910
},
"_version": {
"type": "string",
"description": "API version that produced this representation.",
"example": "4.0"
},
"_links": {
"type": "object",
"description": "`self` — this file record. `tests` — the collection of validation checks run\nagainst it (`/resource/v4/products/{product_id}/files/{file_id}/tests`), which is\nthe itemised form of the summary in `validation`.\n"
},
"type": {
"type": "string",
"description": "What the file is for — a manuscript, an e-book edition, a cover image, a preview.\nThis is the only field that decides how the platform treats the asset, and the only\none that is mandatory on upload; neither the file name nor the MIME type carries that\nmeaning. Twenty values are accepted; anything else is rejected with\n`400 Bad Request`.\n\n**A file record is immutable.** There is no `PUT`. An asset is replaced by a `DELETE`\nof the record followed by a `POST` of the new file under the same `type`. Apart from\n`channel` and `labels`, every remaining field is derived by the server from the\nuploaded bytes, so there is nothing else a client could update.\n\nSource material:\n\n- `dropbox` — author-supplied source manuscript: the document a publication is\n produced *from*, not a deliverable. Unrelated to the Dropbox service.\n\nE-book editions:\n\n- `ebook_epub` — the EPUB edition. Inspected on upload: the version lands in\n `properties.epub_version` and structural features in `labels`.\n- `ebook_pdf` — the PDF edition.\n- `ebook_pdf_cover` — cover page bound into the PDF edition.\n- `ebook_pdf_bastard_title` — half-title (bastard title) page bound into the PDF\n edition.\n- `ebook_mobi` — the Mobipocket / Kindle edition.\n- `ebook_ibook` — the Apple iBooks edition.\n\nPrint-on-demand:\n\n- `pod_bookblock` — the book block, i.e. the interior pages, as a print-ready PDF.\n- `pod_cover` — the cover as one wrap: front, spine and back together.\n- `pod_cover_front` — front cover only.\n- `pod_cover_back` — back cover only.\n\nCovers, previews and samples:\n\n- `cover_marketing` — the marketing cover image. **Must be a JPEG**: the file name\n has to end in `.jpg` or `.jpeg`, or the upload is rejected with `400 Bad Request`.\n The test is on the extension alone, so a PNG renamed to `.jpg` passes it.\n- `extract` — a reading extract offered as a sample of the work.\n- `shop_preview` — HTML preview of the opening pages.\n- `shop_preview_pdf` — a preview offered as a downloadable PDF.\n- `screenshot` — screenshot of the product, for app-like or interactive titles.\n\nAudiobooks:\n\n- `audiobook` — an instruction to unpack a complete audiobook delivery. No record ever\n carries it: an upload with `type=audiobook` is unpacked and each entry becomes a\n record of one of the three types below, chosen from the entry's own media type —\n `audio/*` becomes a track, `image/*` the cover, `application/*` the booklet. Only a\n delivery that unpacks to exactly one record can be answered with that record, so an\n archive of several tracks is stored and then reported as `400 Bad Request`; the\n derived records are visible on the next `GET`. Re-uploading replaces the previously\n derived set.\n- `audiobook_track` — a single audio track of an audiobook.\n- `audiobook_booklet` — the booklet accompanying an audiobook.\n- `audiobook_cover` — cover artwork for an audiobook.\n\nOnly the twenty values listed above are accepted on input, and only they are ever\nreturned.\n\nFilterable — `?type=ebook_epub`; an unknown type is rejected with `400 Bad Request`\nrather than returning an empty result. See\n[Resource Queries](/resource_queries/).\n\n`null` on legacy records that carry no type at all. A legacy record whose type is\nneither absent nor one of the twenty cannot be represented and answers\n`400 Bad Request` when read.\n",
"nullable": true,
"example": "cover_marketing",
"x-enum-descriptions": {
"pod_bookblock": "Print-on-demand book block — the interior pages, print-ready PDF.",
"pod_cover": "Print-on-demand cover — the complete wrap (front, spine, back) as one print-ready PDF.",
"pod_cover_front": "Print-on-demand front cover only.",
"pod_cover_back": "Print-on-demand back cover only.",
"ebook_pdf": "The PDF edition of the e-book, as delivered to customers and channels.",
"ebook_pdf_cover": "Cover page bound into the PDF e-book.",
"ebook_pdf_bastard_title": "Half-title (bastard title) page bound into the PDF e-book.",
"ebook_epub": "The EPUB edition. Automatically inspected on upload: EPUB version lands in `properties.epub_version` and structural features in `labels`.",
"ebook_mobi": "The Mobipocket/Kindle edition.",
"ebook_ibook": "The Apple iBooks edition.",
"cover_marketing": "The marketing cover image. Must be a JPEG — any other format is rejected with `400 Bad Request`.",
"extract": "A reading extract offered as a sample of the work.",
"shop_preview": "HTML preview rendered inside the storefront product page.",
"shop_preview_pdf": "PDF preview offered for download from the storefront.",
"screenshot": "Screenshot of the product, e.g. for app-like or interactive titles.",
"dropbox": "Author-supplied source manuscript. The original document a publication is produced from, not a deliverable.",
"audiobook": "An instruction to unpack a complete audiobook delivery, normally a ZIP. Never carried by a record: each entry becomes an `audiobook_track`, `audiobook_cover` or `audiobook_booklet` record.",
"audiobook_track": "A single audio track of an audiobook.",
"audiobook_booklet": "The booklet accompanying an audiobook.",
"audiobook_cover": "Cover artwork for an audiobook."
}
},
"timestamp": {
"type": "integer",
"description": "When the file was added, as a Unix timestamp (seconds since 1970-01-01 UTC, integer,\nnot a string).\n\nSortable. `id` and `timestamp` are the two field names defined for this resource and\nthe two to rely on; other names may or may not resolve, and one that does not is\nrejected with `400 Bad Request`. For the syntax see\n[Resource Queries](/resource_queries/).\n\nOld records carry the timestamp of the original import, which can predate the platform\nitself, so the two defined orders do not always agree: the oldest file on a\nlong-running title can be decades older than its `_id` ordering suggests.\n",
"example": 1782319836
},
"file_size": {
"type": "integer",
"description": "Size of the stored file in bytes. `null` for records registered by URL, where the\nplatform never receives the bytes.\n\nThere is an upload size limit, and exceeding it answers `413` rather than\n`400 Bad Request`. For a file of 4 GiB or more the value reported here is not the true\nbyte count, even though the file itself is stored whole.\n",
"nullable": true,
"example": 267184
},
"file_name": {
"type": "string",
"description": "The name the file was uploaded under, with any directory part stripped. Informational\nonly — the platform never dispatches on it, and duplicate names within one product are\nallowed. It is echoed back in the `Content-Disposition` header when the content is\ndownloaded, so it is what a browser will save the file as.\n",
"nullable": true,
"example": "v14937.jpg"
},
"mime_type": {
"type": "string",
"description": "Media type of the file. Guessed by the server from the file name extension at\nupload time — the `Content-Type` of the multipart part is ignored, and so is the\ncontent of the file.\n\nThere is no allow-list: an extension the server does not recognise is recorded as\n`application/octet-stream` rather than rejected, so this field never reports failure.\n\n`null` for records registered by URL, whose bytes the platform never receives, and for\nolder records created before media types were recorded. Both are selectable with\n`?mime_type__isnull`.\n\nThis drives which extras appear in `properties`: `image/*` yields `height` and\n`width`, `application/epub+zip` yields `epub_version`.\n\nFilterable — `?mime_type=application/epub+zip`. See\n[Resource Queries](/resource_queries/).\n",
"nullable": true,
"example": "image/jpeg"
},
"labels": {
"type": "array",
"description": "Free-form lowercase tags on the file. Always present, and empty when the file has no\ntags. Client-supplied tags are passed on upload as a comma-separated `labels`\nparameter; they are lowercased and trimmed, and there is no allow-list. They are only\napplied to an uploaded file: a `labels` parameter sent alongside `url` is accepted and\nsilently dropped, leaving the registered record untagged.\n\nTwo families of label are set by the platform itself. The first records how the file\ncame to be:\n\n- `autogenerated` — the file was produced by the platform's own conversion pipeline\n rather than uploaded by a human. Replacing an `autogenerated` file by hand is\n usually futile, because the next conversion run overwrites it again.\n- `book-converter` — names the pipeline that produced the file.\n\nThe second describes the structure of an EPUB. Uploading an `ebook_epub` makes the\nserver open the book and tag it with what it finds:\n\n- `fixedlayout` — fixed-layout rather than reflowable\n- `svgimage` — images supplied as SVG\n- `svg` — SVG content\n- `video` — embedded video\n- `audio` — embedded audio\n- `object` — embedded objects\n\nFilterable by one exact label at a time, equality only: `?labels=autogenerated`\nworks, `?labels=auto` matches nothing, and `?labels__contains=auto` is a\n`400 Bad Request`. A comma-separated list is accepted in a *filter* but always returns\nan empty list, even for a file carrying every label named. One request per label,\nintersected client-side, is the way to express \"and\". See\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"autogenerated"
]
},
"properties": {
"type": "object",
"additionalProperties": true,
"description": "Format-specific attributes the server extracted from the file. The keys depend on\n`mime_type` and are absent — the object is then `{}` — for formats that carry none:\n\n- `height`, `width` — pixel dimensions, **integers**, for any `image/*` file. They\n are the only record of the resolution of a `cover_marketing` image.\n- `epub_version` — `\"2\"` or `\"3\"`, a **string**, for `application/epub+zip`. Determined\n at upload time, and it defaults to `\"2\"` whenever the server did not positively\n identify an EPUB 3, so `\"2\"` means \"not detected as EPUB 3\" rather than \"confirmed\n EPUB 2\".\n\nOne key is filterable: `?property_epub_version=3`. Only `2` and `3` are accepted\nthere; any other value is a `400 Bad Request`. The filter matches the version the\nserver detected, so a record for which no version was ever detected is returned by\nneither `2` nor `3`.\n",
"example": {
"height": 2560,
"width": 1809
}
},
"validation": {
"type": "object",
"additionalProperties": false,
"description": "Rolled-up outcome of every validation check run against this file, or `null` when no\ncheck has ever run — which is the normal state for formats nobody validates, such as\ncover images and HTML previews. `null` therefore means *unknown*, not *passed*.\n\nChecks are not run by the upload request. A file is created with `validation: null`\nand acquires a result seconds to minutes later, once a checker has looked at it, so a\nread-back immediately after an upload still shows `null`. The checkers whose verdicts\nappear here are `pdf-check` for PDFs and `epubchecker` for EPUBs; the per-checker\ndetail behind this summary is at `_links.tests`.\n",
"nullable": true,
"properties": {
"result": {
"type": "string",
"description": "`passed` if every check on the file passed, `failed` if any one of them failed.\nA `failed` file is still stored and still downloadable: this resource reports the\nverdict, it does not act on it.\n",
"example": "failed"
},
"errors": {
"type": "array",
"description": "Every error message from every check, flattened into one list. For EPUBs these\nare EPUBCheck messages and keep their original identifier and location prefix,\nwhich is what identifies the fault inside the source file.\n",
"items": {
"type": "string"
},
"example": [
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
]
},
"warnings": {
"type": "array",
"description": "Every warning message from every check, flattened into one list. Warnings do not make `result` `failed`.",
"items": {
"type": "string"
},
"example": []
}
}
},
"sha1": {
"type": "string",
"description": "SHA-1 digest of the stored bytes, lowercase hex. It is what confirms that a download\nmatches what was stored, and — since file names may repeat and a replaced file keeps\nneither its `_id` nor necessarily a new name — what identifies the bytes themselves.\n`null` for files stored before hashing was introduced, and for URL-registered records\nunless a `sha1` was supplied at creation, in which case the supplied value is returned\nunverified.\n",
"nullable": true,
"example": "9f4806983de39ef7b503c4c658d37427d7caf5f4"
},
"channel": {
"type": "string",
"description": "Restricts the file to one distribution channel, identified by the same lowercase\nchannel slug used elsewhere in the API (`google`, `itunes`, …). It exists for the case\nwhere one channel needs its own variant of an asset — a differently sized cover, a\nwatermarked extract — that must not reach the others.\n\n`null`, the usual case, means the file is not restricted to a channel. Filtering for\nthose records needs the literal, case-sensitive `?channel=NULL`; a plain `?channel=`\ndoes not match them.\n",
"nullable": true,
"example": null
},
"href": {
"type": "string",
"description": "Where the bytes are. For a stored file this is the download endpoint of this API\n(`.../files/{file_id}/file`), and requires the same `Authorization` header as any\nother call — it is not a public or pre-signed link, and it does not expire. For a\nrecord created with `?url=` it is the external URL exactly as registered, which is\nneither fetched nor validated.\n",
"example": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file"
}
},
"required": []
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
DELETE /resource/v4/products/{product_id}/files/{file_id}
Delete a product file
Description
Removes the record and the stored bytes, along with the file's labels and validation
checks. Deletion is the only way to change a file — a re-upload with the same type
follows it.
Deleting one track of an audiobook removes that track only; the other records unpacked from the same archive are unaffected. They are replaced as a set only by uploading a new archive.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
file_id |
string | No | UUID (or legacy integer id) of the file. | |
product_id |
string | No | UUID (or legacy integer id) of the product. |
Responses
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
GET /resource/v4/products/{product_id}/files/{file_id}/file
Download the file content
Description
Streams the stored bytes. Requires the same Authorization header as every other
endpoint; the URL is neither public nor time-limited, so it can be stored and
re-used. The response sets Content-Type from the record's mime_type, falling
back to application/octet-stream, and
Content-Disposition: attachment; filename="<file_name>".
This path does not exist for records registered by url; their bytes are wherever
the external href points.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
file_id |
string | No | UUID (or legacy integer id) of the file. | |
product_id |
string | No | UUID (or legacy integer id) of the product. |
Responses
"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ="
Schema of the response body
{
"type": "string",
"format": "binary"
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
GET /resource/v4/products/{product_id}/files/{file_id}/tests
Retrieve the validation checks of a file
Description
Lists every validation check run against the file, oldest first. This is the
itemised form of the file's validation summary: it names the checker, when it
ran, how long it took, and every message including the info-level ones that the
summary omits.
An empty list corresponds to validation: null on the file.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
file_id |
string | No | UUID (or legacy integer id) of the file. | |
product_id |
string | No | UUID (or legacy integer id) of the product. |
Query parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
errors |
string | No | `?errors=yes` returns only checks that produced at least one error; `?errors=no` only those that produced none. | |
result |
string | No | Filter by verdict — `passed` or `failed`. | |
type |
string | No | Filter by checker, e.g. `?type=epubchecker`. There is at most one record per checker per file. | |
warnings |
string | No | `?warnings=yes` returns only checks that produced at least one warning; `?warnings=no` only those that produced none. |
Responses
{
"_links": {},
"_pagination": {},
"items": [
{
"_id": 1481212,
"type": "epubchecker",
"checker": "Epub Checker",
"result": "failed",
"checked_at": 1699362317,
"duration_in_ms": 23360,
"messages": [
{
"check": "error",
"text": "NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
}
]
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"properties": {
"_links": {
"type": "object"
},
"_pagination": {
"type": "object"
},
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"description": "The result of one validation checker run against one product file.",
"properties": {
"_id": {
"type": "integer",
"description": "Identifier of the check record. Unlike every other v4 resource this is a **plain\ninteger, not a UUID**, and there is no `_legacy_id` alongside it — the check\nresource was never migrated to UUIDs, so the v4 identity convention does not apply\nhere.\n",
"example": 1481212
},
"type": {
"type": "string",
"description": "Which checker produced this record, lowercased. Values are not a closed set, but the\nones that occur are:\n\n- `epubchecker` — the EPUB conformance checker (EPUBCheck)\n- `pdf-check` — the PDF checker\n\nAt most one record exists per (file, `type`) pair, so re-running a checker\noverwrites its previous verdict rather than adding to a history.\n\nFilterable: `?type=epubchecker`. See [Resource Queries](/resource_queries/).\n",
"example": "epubchecker"
},
"checker": {
"type": "string",
"description": "Human-readable name of the tool that ran, for display in an operator UI. `null` when\nthe checker did not identify itself. Unlike `type` this is free text whose casing is\npreserved as the checker reported it, so it is not a value to branch on; `type` is.\n",
"nullable": true,
"example": "Epub Checker"
},
"result": {
"type": "string",
"description": "`passed` or `failed`. A check can be `passed` and still have produced warnings — the\nverdict only reflects errors. If any check on a file is `failed`, the file's\n`validation.result` is `failed` too.\n",
"example": "failed"
},
"checked_at": {
"type": "integer",
"description": "When the check ran, as a Unix timestamp (seconds since 1970-01-01 UTC). Records are\nreturned oldest first by this field. Compared against the file's `timestamp` it shows\nwhether a verdict still describes the bytes currently stored.\n",
"example": 1699362317
},
"duration_in_ms": {
"type": "integer",
"description": "How long the checker took, in milliseconds. EPUB conformance checking unpacks and\nparses the whole book and routinely takes tens of seconds, which is why validation\nresults appear after the upload response rather than in it.\n",
"example": 23360
},
"messages": {
"type": "array",
"description": "Everything the checker reported, in the order it reported it, or `null` when it\nreported nothing at all. An empty list is not used — a silent successful check has\n`messages: null`.\n",
"nullable": true,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"check": {
"type": "string",
"description": "Severity of this message: `error` (makes the check `failed`), `warning`\n(surfaced but harmless to the verdict), or `info` (informational only).\n`error` and `warning` entries are also what the file's `validation.errors` and\n`validation.warnings` lists are built from; `info` entries appear only here.\n",
"example": "error"
},
"text": {
"type": "string",
"description": "The checker's message verbatim. EPUBCheck messages keep their code and source\nlocation (`NCX-001: OPS/toc.ncx(-1:-1): ...`), which is what identifies the\nfault inside the EPUB.\n",
"example": "NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
}
}
}
}
},
"required": []
}
}
},
"required": [
"_links",
"_pagination",
"items"
]
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
GET /resource/v4/products/{product_id}/files/{file_id}/tests/{test_id}
Retrieve a single validation check
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
file_id |
string | No | UUID (or legacy integer id) of the file. | |
product_id |
string | No | UUID (or legacy integer id) of the product. | |
test_id |
string | No | Integer id of the check. Check records have no UUID. |
Responses
{
"_id": 1481212,
"type": "epubchecker",
"checker": "Epub Checker",
"result": "failed",
"checked_at": 1699362317,
"duration_in_ms": 23360,
"messages": [
{
"check": "error",
"text": "NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "The result of one validation checker run against one product file.",
"properties": {
"_id": {
"type": "integer",
"description": "Identifier of the check record. Unlike every other v4 resource this is a **plain\ninteger, not a UUID**, and there is no `_legacy_id` alongside it — the check\nresource was never migrated to UUIDs, so the v4 identity convention does not apply\nhere.\n",
"example": 1481212
},
"type": {
"type": "string",
"description": "Which checker produced this record, lowercased. Values are not a closed set, but the\nones that occur are:\n\n- `epubchecker` — the EPUB conformance checker (EPUBCheck)\n- `pdf-check` — the PDF checker\n\nAt most one record exists per (file, `type`) pair, so re-running a checker\noverwrites its previous verdict rather than adding to a history.\n\nFilterable: `?type=epubchecker`. See [Resource Queries](/resource_queries/).\n",
"example": "epubchecker"
},
"checker": {
"type": "string",
"description": "Human-readable name of the tool that ran, for display in an operator UI. `null` when\nthe checker did not identify itself. Unlike `type` this is free text whose casing is\npreserved as the checker reported it, so it is not a value to branch on; `type` is.\n",
"nullable": true,
"example": "Epub Checker"
},
"result": {
"type": "string",
"description": "`passed` or `failed`. A check can be `passed` and still have produced warnings — the\nverdict only reflects errors. If any check on a file is `failed`, the file's\n`validation.result` is `failed` too.\n",
"example": "failed"
},
"checked_at": {
"type": "integer",
"description": "When the check ran, as a Unix timestamp (seconds since 1970-01-01 UTC). Records are\nreturned oldest first by this field. Compared against the file's `timestamp` it shows\nwhether a verdict still describes the bytes currently stored.\n",
"example": 1699362317
},
"duration_in_ms": {
"type": "integer",
"description": "How long the checker took, in milliseconds. EPUB conformance checking unpacks and\nparses the whole book and routinely takes tens of seconds, which is why validation\nresults appear after the upload response rather than in it.\n",
"example": 23360
},
"messages": {
"type": "array",
"description": "Everything the checker reported, in the order it reported it, or `null` when it\nreported nothing at all. An empty list is not used — a silent successful check has\n`messages: null`.\n",
"nullable": true,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"check": {
"type": "string",
"description": "Severity of this message: `error` (makes the check `failed`), `warning`\n(surfaced but harmless to the verdict), or `info` (informational only).\n`error` and `warning` entries are also what the file's `validation.errors` and\n`validation.warnings` lists are built from; `info` entries appear only here.\n",
"example": "error"
},
"text": {
"type": "string",
"description": "The checker's message verbatim. EPUBCheck messages keep their code and source\nlocation (`NCX-001: OPS/toc.ncx(-1:-1): ...`), which is what identifies the\nfault inside the EPUB.\n",
"example": "NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
}
}
}
}
},
"required": []
}
{
"errors": [
{
"_id": "string",
"stack_trace": [
"string"
],
"message": "string",
"vars": {}
}
]
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"required": [
"errors"
],
"properties": {
"errors": {
"type": "array",
"description": "List of errors occurred",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"_id",
"message",
"stack_trace",
"vars"
],
"properties": {
"_id": {
"type": "string",
"description": "The identifier of the error"
},
"stack_trace": {
"type": "array",
"description": "A stacktrace of the error occurred. Only for testing systems.",
"items": {
"type": "string"
}
},
"message": {
"type": "string",
"description": "A translated message of the error"
},
"vars": {
"type": "object",
"description": "A dict containing additional values of the context of the error."
}
}
}
}
}
}
Schemas
Product File
| Name | Type | Description | Example |
|---|---|---|---|
_id |
string |
UUID of the file record. Stable for the lifetime of the record; a replaced file gets a new one. |
46033f29-9733-45fa-8069-7e7a23dbe864 |
_legacy_id |
integer |
Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. This is the id the v3 API returns as |
20882910 |
_links |
object |
|
|
_version |
string |
API version that produced this representation. |
4.0 |
channel |
string| null |
Restricts the file to one distribution channel, identified by the same lowercase
channel slug used elsewhere in the API (
|
|
file_name |
string| null |
The name the file was uploaded under, with any directory part stripped. Informational
only — the platform never dispatches on it, and duplicate names within one product are
allowed. It is echoed back in the |
v14937.jpg |
file_size |
integer| null |
Size of the stored file in bytes. There is an upload size limit, and exceeding it answers |
267184 |
href |
string |
Where the bytes are. For a stored file this is the download endpoint of this API
( |
https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/files/46033f29-9733-45fa-8069-7e7a23dbe864/file |
labels |
Array<string> |
Free-form lowercase tags on the file. Always present, and empty when the file has no
tags. Client-supplied tags are passed on upload as a comma-separated Two families of label are set by the platform itself. The first records how the file came to be:
The second describes the structure of an EPUB. Uploading an
Filterable by one exact label at a time, equality only: |
[
"autogenerated"
] |
mime_type |
string| null |
Media type of the file. Guessed by the server from the file name extension at
upload time — the There is no allow-list: an extension the server does not recognise is recorded as
This drives which extras appear in Filterable — |
image/jpeg |
properties |
object |
Format-specific attributes the server extracted from the file. The keys depend on
One key is filterable: |
{
"height": 2560,
"width": 1809
} |
sha1 |
string| null |
SHA-1 digest of the stored bytes, lowercase hex. It is what confirms that a download
matches what was stored, and — since file names may repeat and a replaced file keeps
neither its |
9f4806983de39ef7b503c4c658d37427d7caf5f4 |
timestamp |
integer |
When the file was added, as a Unix timestamp (seconds since 1970-01-01 UTC, integer, not a string). Sortable. Old records carry the timestamp of the original import, which can predate the platform
itself, so the two defined orders do not always agree: the oldest file on a
long-running title can be decades older than its |
1782319836 |
type |
string| null |
What the file is for — a manuscript, an e-book edition, a cover image, a preview.
This is the only field that decides how the platform treats the asset, and the only
one that is mandatory on upload; neither the file name nor the MIME type carries that
meaning. Twenty values are accepted; anything else is rejected with
A file record is immutable. There is no Source material:
E-book editions:
Print-on-demand:
Covers, previews and samples:
Audiobooks:
Only the twenty values listed above are accepted on input, and only they are ever returned. Filterable —
|
cover_marketing |
validation |
Properties: result, errors, warnings| null |
Rolled-up outcome of every validation check run against this file, or Checks are not run by the upload request. A file is created with |
|
validation.result |
string |
|
failed |
validation.errors |
Array<string> |
Every error message from every check, flattened into one list. For EPUBs these are EPUBCheck messages and keep their original identifier and location prefix, which is what identifies the fault inside the source file. |
[
"NCX-001: OPS/toc.ncx(-1:-1): NCX identifier (\"com.zeilenquarz.V7\") does not match OPF identifier (\"9783161484100\")."
] |
validation.warnings |
Array<string> |
Every warning message from every check, flattened into one list. Warnings do not make |
[] |
Product File Test
| Name | Type | Description | Example |
|---|---|---|---|
_id |
integer |
Identifier of the check record. Unlike every other v4 resource this is a plain
integer, not a UUID, and there is no |
1481212 |
checked_at |
integer |
When the check ran, as a Unix timestamp (seconds since 1970-01-01 UTC). Records are
returned oldest first by this field. Compared against the file's |
1699362317 |
checker |
string| null |
Human-readable name of the tool that ran, for display in an operator UI. |
Epub Checker |
duration_in_ms |
integer |
How long the checker took, in milliseconds. EPUB conformance checking unpacks and parses the whole book and routinely takes tens of seconds, which is why validation results appear after the upload response rather than in it. |
23360 |
messages |
Array<Properties: check, text>| null |
Everything the checker reported, in the order it reported it, or |
|
messages[].check |
string |
Severity of this message: |
error |
messages[].text |
string |
The checker's message verbatim. EPUBCheck messages keep their code and source
location ( |
NCX-001: OPS/toc.ncx(-1:-1): NCX identifier ("com.zeilenquarz.V7") does not match OPF identifier ("9783161484100"). |
result |
string |
|
failed |
type |
string |
Which checker produced this record, lowercased. Values are not a closed set, but the ones that occur are:
At most one record exists per (file, Filterable: |
epubchecker |