Product Subjects v4
Sub-resource of products holding subject classification — how a title is shelved in a
shop, filed in a catalogue, and found by a reader browsing a category. Each entry is one
scheme / code pair, modelled on the ONIX 3 <Subject> composite: type names the
scheme, text carries the value in that scheme's own vocabulary, and type_onix is the
corresponding scheme identifier from
ONIX Codelist 27, derived by the server from type.
A product normally carries several entries — one or more BISAC headings for the English-language trade, one or more Thema codes for the international trade, a German Warengruppe for the domestic one.
Endpoints
GET /resource/v4/products/{product_id}/subjects
Retrieve subjects for a product
Description
Returns every subject classification entry for the product, paginated. type is the
only field that can be filtered on — ?type=thema for the Thema codes only, for
example; anything else returns HTTP 400. Results cannot be sorted. See
Resource Queries for the filter, sort and pagination syntax.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No | UUID of the product. |
Query parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
type |
string | No | Filter by classification scheme slug — `bisac`, `thema`, `vlb`, or whichever slug has been stored. The only filterable field on this endpoint. The full operator set is supported — see [Resource Queries](/resource_queries/). |
Responses
{
"_links": {},
"_pagination": {},
"items": [
{
"_id": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704",
"_version": "4.0",
"_legacy_id": 918342,
"_links": {},
"type": "bisac",
"text": "HIS002000",
"type_onix": "10",
"channels": []
}
]
}
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 subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"required": []
}
}
},
"required": [
"_links",
"_pagination",
"items"
]
}
POST /resource/v4/products/{product_id}/subjects
Create a new product subject
Description
Adds one classification entry to the product. type and text are both required
and must be non-empty; type_onix is ignored if sent and derived from type;
channels defaults to the empty list, meaning all channels.
The example creates the BISAC heading for a book on Roman history. Adding its Thema
equivalent means a second POST with {"type": "thema", "text": "NHC"} — one entry
per code, never a list.
Nothing prevents duplicates: posting the same type and text twice creates two
entries.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No | UUID of the product. |
Request body
{
"type": "bisac",
"text": "HIS002000",
"channels": []
}
Schema of the request body
{
"type": "object",
"additionalProperties": false,
"description": "One subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"required": []
}
Responses
{
"_id": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704",
"_legacy_id": 918342,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/subjects/b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"type": "bisac",
"text": "HIS002000",
"type_onix": "10",
"channels": []
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"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."
}
}
}
}
}
}
GET /resource/v4/products/{product_id}/subjects/{id}
Retrieve a single product subject
Description
Returns one classification entry by id. The example is a Thema code: NHC is
Ancient history, resolvable at /resource/v4/thema_subjects/NHC, and the derived
type_onix of "93" is what an ONIX 3 <SubjectSchemeIdentifier> would carry.
An id that does not exist returns 404. An id that exists but belongs to a
different product returns 403, not 404 — the record was found, and the
mismatch is treated as a permission failure rather than a lookup failure.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | UUID of the subject entry. | |
product_id |
string | No | UUID of the product. |
Responses
{
"_id": "5c0a7de3-9f41-4b28-8d6a-31f7c2b8e095",
"_legacy_id": 918343,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/subjects/5c0a7de3-9f41-4b28-8d6a-31f7c2b8e095"
},
"type": "thema",
"text": "NHC",
"type_onix": "93",
"channels": []
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"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."
}
}
}
}
}
}
PUT /resource/v4/products/{product_id}/subjects/{id}
Update product subject
Description
Updates one classification entry. Despite being a PUT, this is a partial
update: only the fields present in the body are applied, and every omitted field
keeps its current value. Sending {} therefore changes nothing, and a field cannot
be cleared by leaving it out.
type and text cannot be cleared at all — both must stay non-empty, so "" is
rejected with 400 Bad Request. channels is replaced wholesale when present, and
[] widens an entry back to all channels.
The example corrects a Warengruppe from Geschichte / Vor- und Frühgeschichte (552)
to Geschichte / Altertum (553). Only text is sent; type and channels
are left as they are, and the response returns the complete merged entry. Because
type is unchanged, so is the derived type_onix.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | UUID of the subject entry. | |
product_id |
string | No | UUID of the product. |
Request body
{
"text": "553"
}
Schema of the request body
{
"type": "object",
"additionalProperties": false,
"description": "One subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"required": []
}
Responses
{
"_id": "e3719a4f-6d05-4c93-b1ae-8f24d70c5b16",
"_legacy_id": 918344,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/subjects/e3719a4f-6d05-4c93-b1ae-8f24d70c5b16"
},
"type": "vlb",
"text": "553",
"type_onix": "20",
"channels": []
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One subject classification attached to a product, as a scheme and code pair.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier (UUID) of the subject entry.",
"example": "b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704"
},
"_version": {
"type": "string",
"description": "API Version.",
"example": "4.0"
},
"_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 same record has on the v3 endpoint.",
"example": 918342
},
"_links": {
"type": "object"
},
"type": {
"type": "string",
"description": "The classification scheme this entry belongs to, as a lowercase slug. **Not a closed\nenumeration** — any non-empty string of lowercase letters and hyphens (`[a-z-]`, up\nto 255 bytes) is accepted. Digits, underscores, spaces and upper case are rejected\nwith `400 Bad Request`, as is an empty or missing value.\n\nThree slugs are recognised and cause the server to fill `type_onix`:\n\n| `type` | Scheme | `type_onix` | `text` holds | Look codes up at |\n|---|---|---|---|---|\n| `bisac` | BISAC Subject Headings | `\"10\"` | Nine-character code, e.g. `HIS002000` | `/resource/v4/bisac_subjects/{code}` |\n| `thema` | [Thema](https://ns.editeur.org/thema) subject category | `\"93\"` | Subject code or qualifier, e.g. `NHC`, `1QBA` | `/resource/v4/thema_subjects/{code}` |\n| `vlb` | Warengruppen-Systematik des deutschen Buchhandels | `\"26\"` — but see `type_onix` | Three-digit Warengruppe, e.g. `552` | `/resource/v4/vlb/{id}` |\n| anything else | not recognised | `null` | whatever that scheme calls for | — |\n\nWhat each scheme is:\n\n- **BISAC** Subject Headings, the North American trade classification maintained by the\n [Book Industry Study Group](https://www.bisg.org/complete-bisac-subject-headings-list).\n- **[Thema](https://ns.editeur.org/thema)**, EDItEUR's international multilingual\n scheme, increasingly the European standard. A Thema value may be a subject code\n (`NHC`) or a qualifier (`1QBA`).\n- **Warengruppen-Systematik des deutschen Buchhandels**, the German book trade's\n product-group scheme ([VLB](https://vlb.de/assets/images/wgsneuversion2_0.pdf)).\n\nAnything else is stored as given with `type_onix: null`. That is the supported way to\nrecord a scheme this implementation does not know — BIC\n([Codelist 27](https://ns.editeur.org/onix/en/27) code 12), Dewey (code 01), free\nkeywords (code 20), or a house taxonomy — although an unrecognised slug is only stored\nand returned, is not interpreted anywhere else in the API, and is best agreed with the\nintegration partner in advance.\n\n**There is no main subject.** ONIX 3 marks one `<Subject>` per scheme with the\n`<MainSubject/>` flag, and the product's v3 BISAC and Thema records carry such a flag.\nThis sub-resource has no equivalent field and no ordering guarantee, so the entries it\nreturns are an unranked set in unspecified order. A consumer that needs a primary\ncategory has to establish one by convention — for instance by treating the first BISAC\ncode sent as the main one.\n\n**Not to be confused with `/resource/v4/subjects`.** Three different things in this API\nare called \"subject\", and they do not overlap:\n\n- **This sub-resource** (`/resource/v4/products/{product_id}/subjects`) — industry\n classification codes on one product.\n- **`/resource/v4/subjects`** — the *publisher's own* subject vocabulary: named,\n translatable categories such as *Soziologie* that a realm defines for itself, with\n their own BISAC and VLB mappings. Products are attached to those categories\n elsewhere; this sub-resource does not reference them.\n- **`/resource/v4/bisac_subjects`, `/resource/v4/thema_subjects`, `/resource/v4/vlb`**\n — read-only endpoints exposing the standard code lists, for resolving a code to its\n heading.\n\n**This is the only field the collection endpoint can filter on**, with the comparison\noperators described under [Resource Queries](/resource_queries/). Any other field name\nis rejected with `400 Bad Request`, so `?text=…`, `?type_onix=…` and `?channels=…` all\nfail. Results cannot be sorted: a `sort` parameter is accepted and silently ignored,\nincluding one naming a field that does not exist, so a `200` response is no\nconfirmation that an ordering was applied and the order of results is unspecified.\nPagination is standard, and the response carries `_pagination` and `_links`.\n",
"example": "bisac",
"x-enum-descriptions": {
"bisac": "BISAC Subject Heading — ONIX Codelist 27 code 10. `text` is the nine-character BISAC code, e.g. `HIS002000`.",
"thema": "Thema subject category — ONIX Codelist 27 code 93. `text` is a Thema code or qualifier, e.g. `NHC`, `1QBA`.",
"vlb": "Warengruppen-Systematik des deutschen Buchhandels — ONIX Codelist 27 code 26. `text` is the three-digit Warengruppe, e.g. `552`. See the note on `type_onix`: a `vlb` entry currently returns code 20."
}
},
"text": {
"type": "string",
"description": "The classification value itself, written in the vocabulary of the scheme named by\n`type`. For the recognised schemes that means a code, not a heading:\n\n- `bisac` → `\"HIS002000\"` (HISTORY / Ancient / General)\n- `thema` → `\"NHC\"` (Ancient history), `\"1QBA\"` (Ancient World, a place qualifier)\n- `vlb` → `\"552\"` (Geschichte / Vor- und Frühgeschichte)\n\nFor an unrecognised `type` the field holds whatever that scheme calls for — a code, a\nheading, or a keyword.\n\nNote the German trade convention for `vlb`: a Warengruppe is four characters, but only\nthe last three identify the subject. The leading digit encodes the edition form\n(`1` hardcover/softcover, `2` Taschenbuch, `3` journal/loose-leaf, `9` non-book) and is\nderived from the product's own format, so this field holds the three-digit part, as the\nv3 `vlb` field does.\n\nMust not be empty — a create or update that would leave it empty is rejected with\n`400 Bad Request`. The hard limit is **255 bytes**, not characters — a value carrying\nnon-ASCII characters reaches it sooner than a plain code does — and exceeding it\nreturns `400 Bad Request`. Characters outside the Basic Multilingual Plane are not\nsupported.\n\n**A code is never checked against its scheme.** The value is not validated, not\nnormalised and not de-duplicated: case is preserved exactly as sent, `HIS002000` under\n`type: \"bisac\"` is stored and so is the typo `HIS00200`, and the same code may be\nposted twice for one product. The platform's own vocabularies are the place to check a\ncode before writing it — `GET /resource/v4/bisac_subjects/HIS002000` returns\n`{\"_id\": \"his002000\", \"name\": \"HISTORY / Ancient / General\"}`, and the Thema and VLB\nlookup endpoints behave the same way.\n\n**This sub-resource is a standalone store.** The v3 product resource exposes its own\n`bisac`, `thema`, `vlb` and `keywords` arrays: entries written here are not merged into\nthose arrays, and what those arrays hold does not appear here. An entry is reachable\nthrough this sub-resource and its v3 equivalent, and nowhere else in the API.\n",
"example": "HIS002000"
},
"type_onix": {
"type": "string",
"description": "The [ONIX Codelist 27](https://ns.editeur.org/onix/en/27) subject scheme identifier\ncorresponding to `type`, as a two-character string. Present so that an ONIX consumer\ncan build a `<Subject>` composite without reimplementing the slug mapping:\n`type_onix` becomes `<SubjectSchemeIdentifier>` and `text` becomes `<SubjectCode>`.\n\n**Read-only in effect.** The field is accepted in a request body but the value is\ndiscarded — the server recomputes it from `type` on every write. It is `null`\nwhenever `type` is not one of the three recognised slugs. To change it, change `type`.\n\nThe intended mapping is:\n\n- `bisac` → `\"10\"` — BISAC Subject Heading\n- `thema` → `\"93\"` — Thema subject category\n- `vlb` → `\"26\"` — Warengruppen-Systematik des deutschen Buchhandels\n\n**Known issue:** a `vlb` entry currently returns `\"20\"` (Keywords) rather than `\"26\"`.\nUntil this is corrected, the scheme of a `vlb` entry cannot be inferred from\n`type_onix`; `type` is the reliable indicator.\n",
"nullable": true,
"example": "10"
},
"channels": {
"type": "array",
"description": "Distribution channels this classification is intended for. An **empty list means the\nentry applies to every channel** and is the normal case; it is populated only when one\npartner needs a different classification from everyone else — for example a retailer\nthat wants a BISAC heading nobody else should see.\n\nValues are the platform's distribution-channel slugs. Those in common use are:\n\n- `google`\n- `amazon`\n- `kobo`\n- `spotify`\n- `libri`\n- `ciando`\n- `divibib`\n- `overdrive`\n- `scribd`\n- `bookwire`\n\nThe list is open-ended — a realm distributes through whichever partners it has\ncontracts with. **They are not validated against the channel list** — any string of\nlowercase letters and hyphens is accepted, so a typo silently produces an entry no\nchannel will ever match. Channel slugs containing digits or underscores (`24symbols`,\n`manual_sale`, `zeitfracht_sa02e`) are valid channels elsewhere in the API but are\n**rejected** here with `400 Bad Request`, because this field allows only `[a-z-]`.\n\nThe list is normalised on write: values are lower-cased, de-duplicated and sorted\nalphabetically. Sending `[\"Google\", \"amazon\", \"google\"]` stores and returns\n`[\"amazon\", \"google\"]`. On update the list is replaced wholesale, never merged.\n\nThe collection endpoint cannot filter on this field — see the note on filtering under\n`type`.\n",
"items": {
"type": "string"
},
"example": []
}
},
"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."
}
}
}
}
}
}
DELETE /resource/v4/products/{product_id}/subjects/{id}
Delete product subject
Description
Removes one classification entry. There is no soft delete and no way to remove a whole scheme in one call; entries are deleted individually.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | UUID of the subject entry. | |
product_id |
string | No | UUID 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."
}
}
}
}
}
}
Schemas
Product Subject
| Name | Type | Description | Example | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
_id |
string |
Unique identifier (UUID) of the subject entry. |
b1f4c0d6-2ac9-4e17-9f3d-6c81a2e5b704 |
|||||||||||||||||||||||||
_legacy_id |
integer |
Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. This is the |
918342 |
|||||||||||||||||||||||||
_links |
object | |||||||||||||||||||||||||||
_version |
string |
API Version. |
4.0 |
|||||||||||||||||||||||||
channels |
Array<string> |
Distribution channels this classification is intended for. An empty list means the entry applies to every channel and is the normal case; it is populated only when one partner needs a different classification from everyone else — for example a retailer that wants a BISAC heading nobody else should see. Values are the platform's distribution-channel slugs. Those in common use are:
The list is open-ended — a realm distributes through whichever partners it has
contracts with. They are not validated against the channel list — any string of
lowercase letters and hyphens is accepted, so a typo silently produces an entry no
channel will ever match. Channel slugs containing digits or underscores ( The list is normalised on write: values are lower-cased, de-duplicated and sorted
alphabetically. Sending The collection endpoint cannot filter on this field — see the note on filtering under
|
[] |
|||||||||||||||||||||||||
text |
string |
The classification value itself, written in the vocabulary of the scheme named by
For an unrecognised Note the German trade convention for Must not be empty — a create or update that would leave it empty is rejected with
A code is never checked against its scheme. The value is not validated, not
normalised and not de-duplicated: case is preserved exactly as sent, This sub-resource is a standalone store. The v3 product resource exposes its own
|
HIS002000 |
|||||||||||||||||||||||||
type |
string |
The classification scheme this entry belongs to, as a lowercase slug. Not a closed
enumeration — any non-empty string of lowercase letters and hyphens ( Three slugs are recognised and cause the server to fill
What each scheme is:
Anything else is stored as given with There is no main subject. ONIX 3 marks one Not to be confused with
This is the only field the collection endpoint can filter on, with the comparison
operators described under Resource Queries. Any other field name
is rejected with |
bisac |
|||||||||||||||||||||||||
type_onix |
string| null |
The ONIX Codelist 27 subject scheme identifier
corresponding to Read-only in effect. The field is accepted in a request body but the value is
discarded — the server recomputes it from The intended mapping is:
Known issue: a |
10 |