Product Prices v4
Sub-resource of products holding the price entries of a title. Each entry is one amount in one currency, scoped to a single product format, a territory, an optional set of distribution channels and an optional validity period. A product normally carries several entries, covering different countries, currencies and pricing models.
qualifier_code, a value from ONIX
Codelist 59, separates the three kinds of entry
this sub-resource accepts: a regular consumer price (no qualifier), a library or
institutional price ("06"), and a time-limited campaign price ("08").
Endpoints
GET /resource/v4/products/{product_id}/prices
Retrieve prices for a product
Description
Returns all price entries for the given product, paginated. Query parameters narrow
the result — ?qualifier_code=06 for library prices, ?qualifier_code=08 for
campaign prices, ?channels__contains=divibib for the prices of one channel.
Seven fields are filterable: product_type, qualifier_code, price_code,
channels, the currency (as either currency or currency_code),
country_codes_included and region_codes_included. The last two accept equality
and __contains only. Any other query parameter is rejected with
400 Bad Request; product_id is part of the path, not a filter. See
Resource Queries for the operator and sort syntax.
Regular consumer prices carry qualifier_code: null, so they are selected with
?qualifier_code__isnull rather than by a code value, and ?qualifier_code__ne=08
does not return them.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No | UUID of the product. |
Query parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
channels__contains |
string | No | Filter to prices whose `channels` list contains the given value (e.g. `divibib`, `overdrive`). | |
country_codes_included |
string | No | Exact match on included country codes. Takes a comma-separated list of ISO 3166-1 alpha-2 codes (e.g. `DE,AT`). Order does not matter, because the codes are sorted before they are compared. | |
country_codes_included__contains |
string | No | Substring match within the included country codes list (e.g. `D` matches both `DE` and `DK`). | |
currency |
string | No | Filter by ISO 4217 currency code (e.g. `EUR`, `USD`, `CHF`). | |
qualifier_code |
string | No | Filter by ONIX price type qualifier ([Codelist 59](https://ns.editeur.org/onix/en/59)). `06` selects library / institutional prices and `08` campaign prices. Regular consumer prices carry no qualifier at all and are selected with `?qualifier_code__isnull` instead of a code value. | |
region_codes_included__contains |
string | No | Substring match within the included region codes list. `WORLD` is the only region code the platform's territory handling supports, so in practice it is the only value worth matching. |
Responses
{
"_links": {},
"_pagination": {},
"items": [
{
"_id": "949b0070-d590-4154-bbb9-961051f3f36a",
"_version": "4.0",
"_legacy_id": 38054816,
"_links": {},
"description": "geb. Ladenpreis",
"product_type": "ebook",
"qualifier_code": null,
"price_code": null,
"price_type": "fixed",
"price_status_code": "02",
"vat_type": "auto",
"gross_price": true,
"country_codes_included": [
"DE",
"AT"
],
"country_codes_excluded": [],
"region_codes_included": [],
"region_codes_excluded": [],
"effective_from": "2026-08-06",
"effective_until": "2026-09-06",
"channels": [],
"price": {
"value": 42.23,
"currency": "EUR"
}
}
]
}
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 price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"required": []
}
}
},
"required": [
"_links",
"_pagination",
"items"
]
}
POST /resource/v4/products/{product_id}/prices
Create a new product price
Description
Creates one price entry for the product. Fields omitted from the body take their default value.
The example below creates a library price: a divibib quota licence, priced net
(gross_price: false) and scoped to the divibib channel.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
product_id |
string | No |
Request body
{
"description": "K-Lizenz",
"product_type": "ebook",
"qualifier_code": "06",
"price_code": "quota-license",
"price_type": null,
"price_status_code": null,
"vat_type": "auto",
"gross_price": false,
"country_codes_included": [],
"country_codes_excluded": [],
"region_codes_included": [
"WORLD"
],
"region_codes_excluded": [],
"channels": [
"divibib"
],
"effective_from": "2024-03-12",
"effective_until": null,
"price": {
"value": 4.19,
"currency": "EUR"
}
}
Schema of the request body
{
"type": "object",
"additionalProperties": false,
"description": "One price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"required": []
}
Responses
{
"_id": "7ebf6af1-59f2-4958-88ed-8858e588981d",
"_legacy_id": 7351,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/ea8af2fa-e616-11ee-a221-00505699575c/prices/7ebf6af1-59f2-4958-88ed-8858e588981d"
},
"description": "K-Lizenz",
"product_type": "ebook",
"qualifier_code": "06",
"price_code": "quota-license",
"price_type": null,
"price_status_code": null,
"vat_type": "auto",
"gross_price": false,
"country_codes_included": [],
"country_codes_excluded": [],
"region_codes_included": [
"WORLD"
],
"region_codes_excluded": [],
"channels": [
"divibib"
],
"effective_from": "2024-03-12",
"effective_until": null,
"price": {
"value": 4.19,
"currency": "EUR"
}
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"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}/prices/{id}
Retrieve a single product price
Description
Returns one price entry by id. The example below is a campaign price: the e-book's regular price is 13.99 EUR, and this entry discounts it to 9.55 EUR for the month of the promotion. The regular entry is untouched and applies again on 7 September.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | ||
product_id |
string | No |
Responses
{
"_id": "a29fddb1-0b06-4239-aedb-1eb59deb7bda",
"_legacy_id": 38054817,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/prices/a29fddb1-0b06-4239-aedb-1eb59deb7bda"
},
"description": "Summer campaign",
"product_type": "ebook",
"qualifier_code": "08",
"price_code": null,
"price_type": null,
"price_status_code": null,
"vat_type": "auto",
"gross_price": true,
"country_codes_included": [],
"country_codes_excluded": [],
"region_codes_included": [
"WORLD"
],
"region_codes_excluded": [],
"channels": [],
"effective_from": "2026-08-06",
"effective_until": "2026-09-06",
"price": {
"value": 9.55,
"currency": "EUR"
}
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"required": []
}
PUT /resource/v4/products/{product_id}/prices/{id}
Update product price
Description
Updates one price entry. Despite being a PUT, this is a partial update: only
the fields present in the body are applied, and every field absent from it keeps
its current value. Sending {} therefore changes nothing, and a nullable field is
cleared only by an explicit null.
The example below is a regular consumer price being raised from 13.99 EUR to
14.99 EUR. Only price is sent; product_type, territory, VAT handling and the
rest stay as they are. The response returns the complete merged entry.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | ||
product_id |
string | No |
Request body
{
"price": {
"value": 14.99,
"currency": "EUR"
}
}
Schema of the request body
{
"type": "object",
"additionalProperties": false,
"description": "One price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"required": []
}
Responses
{
"_id": "19cecc47-32e7-4e29-8e24-7b2f4e6b9f37",
"_legacy_id": 38054816,
"_version": "4.0",
"_links": {
"self": "https://api.openpublishing.com/resource/v4/products/a6fc09af-e616-11ee-a221-00505699575c/prices/19cecc47-32e7-4e29-8e24-7b2f4e6b9f37"
},
"description": null,
"product_type": "ebook",
"qualifier_code": null,
"price_code": null,
"price_type": null,
"price_status_code": null,
"vat_type": "auto",
"gross_price": true,
"country_codes_included": [],
"country_codes_excluded": [],
"region_codes_included": [
"WORLD"
],
"region_codes_excluded": [],
"channels": [],
"effective_from": null,
"effective_until": null,
"price": {
"value": 14.99,
"currency": "EUR"
}
}
Schema of the response body
{
"type": "object",
"additionalProperties": false,
"description": "One price entry of a product, scoped to a format, a territory, a channel set and a period.",
"properties": {
"_id": {
"type": "string",
"description": "Unique identifier for the price.",
"example": "949b0070-d590-4154-bbb9-961051f3f36a"
},
"_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.",
"example": 38054816
},
"_links": {
"type": "object"
},
"description": {
"type": "string",
"description": "Human-readable label for the price entry, typically describing its purpose or origin.\nExamples from the book trade: `\"geb. Ladenpreis\"` (fixed retail price, gross),\n`\"Netto-Ladenpreis\"` (net retail price), `\"K-Lizenz\"` (quota license for libraries).\n\nFor campaign prices (`qualifier_code: \"08\"`) this holds the promotion's name —\n`\"Summer campaign\"`, `\"Christmas\"` — since it is the only field that distinguishes one\ncampaign of a product from another.\n",
"nullable": true,
"example": "geb. Ladenpreis"
},
"product_type": {
"type": "string",
"description": "The product format this price applies to. Each format is priced independently, so a\nsingle product may carry separate ebook and book prices. One of four values:\n\n- `ebook` — e-book in any file format (EPUB, PDF, …)\n- `book` — physical book, including print-on-demand\n- `audiobook` — audiobook, downloadable or streaming\n- `nonbook` — non-book article\n\nAlways present in a response; a price cannot exist without a format. Values are\nlower-cased on write, so `\"Ebook\"` is accepted and comes back as `ebook`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"example": "ebook",
"x-enum-descriptions": {
"ebook": "E-book in any file format (EPUB, PDF, …)",
"book": "Physical book, including print-on-demand",
"audiobook": "Audiobook, downloadable or streaming",
"nonbook": "Non-book article"
}
},
"qualifier_code": {
"type": "string",
"description": "ONIX price type qualifier from [Codelist 59](https://ns.editeur.org/onix/en/59),\nidentifying the audience or purpose of this price. `null` is an unqualified consumer\nprice, equivalent to Codelist 59 code `\"00\"`. Codelist 59 defines twenty codes, but\nonly three qualifier modes are accepted; any other value is rejected with `400 Bad\nRequest`:\n\n- `null` — regular consumer price (retail).\n- `\"06\"` — corporate / library / education price, for sale to libraries, academic\n institutions or other corporate customers. The most common non-null qualifier,\n normally combined with a `price_code` and a `channels` entry.\n- `\"08\"` — promotional offer price: a time-limited campaign price bounded by\n `effective_from` and `effective_until`, and named in `description`.\n\nA campaign does not shorten, modify or replace the regular price entry. The regular\nprice simply stops being the effective one for the duration of the campaign and\napplies again the day after `effective_until`, without being re-created or amended.\nUnlike regular and library prices, campaigns need not tile the timeline without gaps,\nso a product may carry several unrelated campaign windows with ordinary pricing\nbetween them.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax. Because regular prices\ncarry `null`, they are selected with `?qualifier_code__isnull` and are not returned by\n`?qualifier_code__ne=08`.\n",
"nullable": true,
"example": null,
"x-enum-descriptions": {
"06": "Corporate / Library / Education price — for sale to libraries, academic institutions, or other corporate customers (ONIX Codelist 59).",
"08": "Promotional offer price — a time-limited campaign price that overrides the regular price for the period between `effective_from` and `effective_until` (ONIX Codelist 59)."
}
},
"price_code": {
"type": "string",
"description": "A lowercase identifier for the pricing or licensing model, used primarily with library\nprices (`qualifier_code: \"06\"`). Lower-cased on write, and restricted to lowercase\nletters and hyphens (`[a-z-]`); anything else is rejected with `400 Bad Request`.\nCommon values in the book trade:\n\n- `pay-per-loan` — the library pays a fee each time the title is lent to a patron.\n- `quota-license` — the library purchases a fixed number of loans, or a time-limited\n licence for a set of concurrent users.\n\nRegular consumer prices typically have `price_code: null`.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"nullable": true,
"example": null
},
"price_type": {
"type": "string",
"description": "Whether this is a recommended retail price or a fixed (legally binding) retail price.\nOne of two values, or `null` when the distinction is not applicable. Anything else is\nrejected with `400 Bad Request`. Both correspond to a pair of codes in\n[ONIX Codelist 58](https://ns.editeur.org/onix/en/58), one for a tax-exclusive and one\nfor a tax-inclusive amount, which is the distinction `gross_price` records:\n\n- `recommended` — Recommended Retail Price (Codelist 58 `01` excluding tax, `02`\n including tax). A retailer may sell at a different price.\n- `fixed` — Fixed Retail Price (Codelist 58 `03` excluding tax, `04` including tax).\n The price is legally binding, for instance under German and Austrian fixed book\n price law (*Buchpreisbindung*).\n",
"nullable": true,
"example": "fixed"
},
"price_status_code": {
"type": "string",
"description": "ONIX price status code from [Codelist 61](https://ns.editeur.org/onix/en/61), stating\nhow settled the price is. Any Codelist 61 code is accepted; anything else is rejected\nwith `400 Bad Request`. The codelist has three entries:\n\n- `\"00\"` — Unspecified. The codelist's own default.\n- `\"01\"` — Provisional: the price is not yet settled.\n- `\"02\"` — Confirmed: the price is settled and in effect. The common value.\n\n`null` when no status is recorded.\n",
"nullable": true,
"example": "02"
},
"vat_type": {
"type": "string",
"description": "How VAT (Value Added Tax) is determined for this price. One of four values; anything\nelse is rejected with `400 Bad Request`:\n\n- `auto` — VAT is calculated from the destination country, the product format\n (e-books and print books often carry different rates) and the date. The default,\n and the usual setting.\n- `standard` — the standard VAT rate is applied regardless of product format.\n- `reduced` — the reduced VAT rate is applied.\n- `zero` — no VAT is applied.\n\nAlways present in a response; `auto` applies when the request body omits the field.\n",
"example": "auto",
"x-enum-descriptions": {
"auto": "VAT calculated automatically from country, product format, and date (default).",
"standard": "Standard VAT rate applied.",
"reduced": "Reduced VAT rate applied.",
"zero": "No VAT."
}
},
"gross_price": {
"type": "boolean",
"description": "`true` if `price.value` is a gross price (tax included), `false` if it is a net price\n(tax excluded). Gross prices are typical for consumer-facing retail in countries with\nfixed book price laws; net prices are common for international or institutional sales,\nwhere the buyer settles tax itself. Together with `price_type` this decides which of\nthe two ONIX Codelist 58 codes the entry corresponds to.\n"
},
"country_codes_included": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes where this price applies (e.g. `\"DE\"`, `\"AT\"`,\n`\"CH\"`). Each code is checked against the countries the platform recognises, so an\nunrecognised code is rejected with `400 Bad Request`. Values are uppercased,\nde-duplicated and sorted alphabetically on write, so the order in a response need not\nmatch the order sent, and on update the list is replaced wholesale rather than merged.\n\nAlways present in a response, but frequently `[]`, which is meaningful: an empty list\nmeans the price is not country-specific, not that its territory is unknown —\n`region_codes_included` carries the scope instead, typically as `[\"WORLD\"]`. When a\nrequest leaves both include lists empty, `region_codes_included` comes back as\n`[\"WORLD\"]`.\n\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": [
"DE",
"AT"
]
},
"country_codes_excluded": {
"type": "array",
"description": "ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and\nnormalised exactly as `country_codes_included` is. It carves countries out of the\nincluded set, which is what defines a broad price with country-level exceptions:\n`region_codes_included: [\"WORLD\"]` together with `country_codes_excluded: [\"DE\"]`\nmeans every country except Germany.\n\nAlways present in a response; `[]` means nothing is carved out. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_included": {
"type": "array",
"description": "Region codes where this price applies, validated against ONIX\n[Codelist 49](https://ns.editeur.org/onix/en/49); a code outside that codelist is\nrejected with `400 Bad Request`. In practice `WORLD` is the only usable value, because\nthe platform's territory handling rejects every other Codelist 49 region. `ROW` is\naccepted as a synonym and comes back as `WORLD`.\n\n`[\"WORLD\"]` makes the price the fallback for every country that has no more specific\ncountry-level price, minus anything listed in `country_codes_excluded`. An empty list\nmeans the price is already scoped by `country_codes_included`; when both include lists\nare empty, `[\"WORLD\"]` comes back.\n\nValues are uppercased, de-duplicated and sorted on write. Always present in a response.\nFilterable on the collection endpoint, with equality and `__contains` only; see\n[Resource Queries](/resource_queries/).\n",
"items": {
"type": "string"
},
"example": []
},
"region_codes_excluded": {
"type": "array",
"description": "Region codes explicitly excluded from this price. Always `[]` in practice: the\nplatform's territory handling does not support a non-empty value, so exclusions are\nexpressed through `country_codes_excluded` instead. Values are uppercased,\nde-duplicated and sorted on write. Always present in a response. Not filterable.\n",
"items": {
"type": "string"
},
"example": []
},
"effective_from": {
"type": "string",
"description": "The date from which this price becomes effective, in `YYYY-MM-DD` format. `null` means\nthe price has no start date and is effective from the beginning of time.\n\nPrices are grouped by product format, `qualifier_code`, territory and channel, and\nwithin one group the effective periods are expected to form a consistent timeline: no\ntwo periods overlap, and only the earliest price of the group leaves this field open.\nRegular and library prices should additionally form a *gapless* chain, each period\nstarting the day after the previous one ends, so that no date is left without a price.\nCampaign prices (`qualifier_code: \"08\"`) are the exception: each is an isolated window\nwhose first day is set here, and gaps between campaigns are normal.\n\nThis endpoint validates each price entry on its own and does not compare it with the\nother prices of the product, so an overlapping or gapped timeline is accepted on\nwrite.\n",
"nullable": true,
"example": "2026-08-06"
},
"effective_until": {
"type": "string",
"description": "The date until which this price remains effective, in `YYYY-MM-DD` format. The day\nitself is included. `null` means the price has no end date and remains effective\nindefinitely; only the latest price of a group leaves this field open.\n\nFor a campaign price (`qualifier_code: \"08\"`) this is the last day of the promotion.\nThe regular price applies again on the following day, without the regular entry being\nre-created or amended.\n",
"nullable": true,
"example": "2026-09-06"
},
"channels": {
"type": "array",
"description": "Distribution channel identifiers this price is scoped to. An empty list — the normal\ncase — means the price applies to every channel. Only library prices\n(`qualifier_code: \"06\"`) use it in practice, naming a lending platform; regular\nconsumer prices carry an empty list.\n\nValues are the platform's distribution-channel slugs, for example:\n\n- `divibib`\n- `overdrive`\n- `ciando`\n- `skoobe`\n- `libri`\n- `google`\n\nThey are not validated against that list — any string of lowercase letters and hyphens\nis accepted, so a typo silently produces a price no channel matches. Slugs containing\ndigits or underscores (`24symbols`, `manual_sale`, `zeitfracht_sa02e`) are valid\nchannels elsewhere in the API but are rejected here with `400 Bad Request`, because\nthis field allows only `[a-z-]`.\n\nThe list is lower-cased, de-duplicated and sorted alphabetically on write, so the order\nin a response need not match the order sent. On update it is replaced wholesale, never\nmerged. Always present in a response.\n\nFilterable on the collection endpoint; see\n[Resource Queries](/resource_queries/) for the operator syntax.\n",
"items": {
"type": "string"
},
"example": []
},
"price": {
"type": "object",
"description": "Monetary Amount",
"additionalProperties": false,
"nullable": true,
"properties": {
"value": {
"type": "number",
"description": "Value of the monetary amount",
"example": 42.23
},
"currency": {
"type": "string",
"description": "ISO 4217 currency code",
"example": "EUR"
}
},
"required": [
"value",
"currency"
]
}
},
"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}/prices/{id}
Delete product price
Description
Removes the price entry. No check is made that the remaining entries still cover the timeline.
Input parameters
Path parameters
| Parameter | Type | Default | Nullable | Description |
|---|---|---|---|---|
id |
string | No | ||
product_id |
string | No |
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."
}
}
}
}
}
}
Schemas
Product Price
| Name | Type | Description | Example |
|---|---|---|---|
_id |
string |
Unique identifier for the price. |
949b0070-d590-4154-bbb9-961051f3f36a |
_legacy_id |
integer |
Legacy integer identifier of this resource, kept so existing integrations can correlate their own records. |
38054816 |
_links |
object | ||
_version |
string |
API Version. |
4.0 |
channels |
Array<string> |
Distribution channel identifiers this price is scoped to. An empty list — the normal
case — means the price applies to every channel. Only library prices
( Values are the platform's distribution-channel slugs, for example:
They are not validated against that list — any string of lowercase letters and hyphens
is accepted, so a typo silently produces a price no channel matches. Slugs containing
digits or underscores ( The list is lower-cased, de-duplicated and sorted alphabetically on write, so the order in a response need not match the order sent. On update it is replaced wholesale, never merged. Always present in a response. Filterable on the collection endpoint; see Resource Queries for the operator syntax. |
[] |
country_codes_excluded |
Array<string> |
ISO 3166-1 alpha-2 country codes explicitly excluded from this price, validated and
normalised exactly as Always present in a response; |
[] |
country_codes_included |
Array<string> |
ISO 3166-1 alpha-2 country codes where this price applies (e.g. Always present in a response, but frequently Filterable on the collection endpoint, with equality and |
[
"DE",
"AT"
] |
description |
string| null |
Human-readable label for the price entry, typically describing its purpose or origin.
Examples from the book trade: For campaign prices ( |
geb. Ladenpreis |
effective_from |
string| null |
The date from which this price becomes effective, in Prices are grouped by product format, This endpoint validates each price entry on its own and does not compare it with the other prices of the product, so an overlapping or gapped timeline is accepted on write. |
2026-08-06 |
effective_until |
string| null |
The date until which this price remains effective, in For a campaign price ( |
2026-09-06 |
gross_price |
boolean |
|
|
price |
Properties: value, currency| null |
Monetary Amount |
|
price.value |
number |
Value of the monetary amount |
42.23 |
price.currency |
string |
ISO 4217 currency code |
EUR |
price_code |
string| null |
A lowercase identifier for the pricing or licensing model, used primarily with library
prices (
Regular consumer prices typically have Filterable on the collection endpoint; see Resource Queries for the operator syntax. |
|
price_status_code |
string| null |
ONIX price status code from Codelist 61, stating
how settled the price is. Any Codelist 61 code is accepted; anything else is rejected
with
|
02 |
price_type |
string| null |
Whether this is a recommended retail price or a fixed (legally binding) retail price.
One of two values, or
|
fixed |
product_type |
string |
The product format this price applies to. Each format is priced independently, so a single product may carry separate ebook and book prices. One of four values:
Always present in a response; a price cannot exist without a format. Values are
lower-cased on write, so Filterable on the collection endpoint; see Resource Queries for the operator syntax. |
ebook |
qualifier_code |
string| null |
ONIX price type qualifier from Codelist 59,
identifying the audience or purpose of this price.
A campaign does not shorten, modify or replace the regular price entry. The regular
price simply stops being the effective one for the duration of the campaign and
applies again the day after Filterable on the collection endpoint; see
Resource Queries for the operator syntax. Because regular prices
carry |
|
region_codes_excluded |
Array<string> |
Region codes explicitly excluded from this price. Always |
[] |
region_codes_included |
Array<string> |
Region codes where this price applies, validated against ONIX
Codelist 49; a code outside that codelist is
rejected with
Values are uppercased, de-duplicated and sorted on write. Always present in a response.
Filterable on the collection endpoint, with equality and |
[] |
vat_type |
string |
How VAT (Value Added Tax) is determined for this price. One of four values; anything
else is rejected with
Always present in a response; |
auto |