Skip to content

Product Storefronts v4

Sub-resource of products recording which shops a product is offered in, and how it presents itself in each of them. A realm that sells directly may run a single shop or several — a general one alongside specialised ones for particular subject areas or product forms — and this resource is the per-shop on/off switch, together with the reading sample and the search-engine metadata that go with it.

Each record is the link between one product and one shop, so a product carries at most one record per shop. The shop itself is a separate top-level resource, /resource/v4/storefronts, and is embedded in every record here as storefront.


Endpoints


GET /resource/v4/products/{product_id}/storefronts

Retrieve the storefronts of a product

Description

The collection returns one entry per storefront of the realm, not one entry per shop the title is in. A shop the product was never added to still appears, as a placeholder: _id is the empty string, enabled is false, preview_percentage is 0 and the SEO fields are null. The number of entries is therefore the number of shops the realm runs, and enabled is the field that says whether the title is offered in each.

The envelope is a bare {"items": [...]} — no _links, no _pagination.

This resource supports no filtering, sorting or pagination, so none of the syntax in Resource Queries applies to it. Every query parameter is ignored, silently and without error: ?enabled=true, ?display=2, ?page=2, a sort parameter and an outright nonsense parameter all return the identical full list with 200. The list is bounded by the handful of shops a realm runs, so filtering belongs on the client. The order of items is not part of the contract.

In the example below the title is offered in the publisher's main shop but not in its specialist one. Two things in the second entry are easy to misread: its html_* fields are null because the entry is disabled rather than because the product has no metadata — the first entry shows that it does — and its url is the first shop's URL, because that field falls back to whatever shop the product is in rather than reporting null.

Input parameters

Path parameters

Parameter Type Default Nullable Description
product_id string No UUID (or legacy integer id) of the product.

Responses

{
    "items": [
        {
            "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
            "enabled": true,
            "free_of_charge": false,
            "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
            "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
            "html_title": null,
            "preview_percentage": 25,
            "storefront": {
                "_id": "9ee61a17-e617-11ee-a221-00505699575c",
                "_links": {
                    "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
                },
                "name": "zeilenquarz"
            },
            "url": "https://www.example.com/document/14937"
        },
        {
            "_id": "",
            "enabled": false,
            "free_of_charge": false,
            "html_meta_description": null,
            "html_meta_keywords": null,
            "html_title": null,
            "preview_percentage": 0,
            "storefront": {
                "_id": "9ee62740-e617-11ee-a221-00505699575c",
                "_links": {
                    "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee62740-e617-11ee-a221-00505699575c"
                },
                "name": "zeilenquarz-fachbuch"
            },
            "url": "https://www.example.com/document/14937"
        }
    ]
}
Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "items": {
            "type": "array",
            "items": {
                "type": "object",
                "additionalProperties": false,
                "description": "One product's presence in one storefront — the link between a product and a shop.",
                "example": {
                    "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
                    "storefront": {
                        "_id": "9ee61a17-e617-11ee-a221-00505699575c",
                        "_links": {
                            "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
                        },
                        "name": "zeilenquarz"
                    },
                    "url": "https://www.example.com/document/14937",
                    "enabled": true,
                    "preview_percentage": 25,
                    "html_title": null,
                    "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
                    "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
                    "free_of_charge": false
                },
                "properties": {
                    "_id": {
                        "type": "string",
                        "description": "UUID of this product-to-storefront link. It identifies the link, not the shop — the\nshop's own id is `storefront._id`, and passing that one to the single-record endpoint\nis a `404`.\n\n**The empty string** marks a placeholder — one of the entries the collection returns\nfor a shop the product was never added to. `\"\"` means \"not in this shop\", and such an\nentry is not addressable: appending the empty string to the single-record path is a\n`404`, so the record has to be created with `POST` first.\n\nThe single-record path accepts this UUID and nothing else; a legacy integer id does not\nresolve there. Storefront links carry no `_legacy_id` in any case: this resource\nidentified itself by UUID already in v3, so v3 and v4 return the same `_id`. Records\nhere also carry neither a `_version` nor a record-level `_links`, both of which most v4\nresources have.\n",
                        "example": "b62d8555-5fa4-11ef-a221-00505699575c"
                    },
                    "storefront": {
                        "type": "object",
                        "additionalProperties": false,
                        "description": "The shop this record is about — the same representation the top-level\n`/resource/v4/storefronts/{id}` endpoint returns. Realms typically run several:\na main shop plus specialised ones for particular subject areas or product forms.\n\n`{\"storefront\": {\"_id\": \"<uuid>\"}}` is what says which shop a record is for: on `POST`\nit attaches the new record to a shop, and on `PUT` it moves an existing record to\nanother shop of the same realm. Only `_id` is read; a `name` sent alongside it is\nignored. The shop must belong to the same realm as the product, and one that does not —\nlike one that does not exist at all — is rejected with `400 Bad Request`.\n\n`storefront` and `enabled` are the only two fields `PUT` and `POST` read from the body.\nEverything else sent is discarded without comment.\n",
                        "properties": {
                            "_id": {
                                "type": "string",
                                "description": "UUID of the storefront. This is the id to use against `/resource/v4/storefronts/{id}`, and the one to send when creating a record.",
                                "example": "9ee61a17-e617-11ee-a221-00505699575c"
                            },
                            "_links": {
                                "type": "object",
                                "description": "`self` — the storefront's own resource, `/resource/v4/storefronts/{storefront_id}`."
                            },
                            "name": {
                                "type": "string",
                                "description": "The shop's short name: a lowercase slug, unique within the realm, used as the\nstorefront's handle throughout the platform rather than as a display title. It\nis normally the first component of the shop's hostname, which is why the `url`\nof an entry usually echoes it.\n",
                                "example": "zeilenquarz"
                            }
                        }
                    },
                    "url": {
                        "type": "string",
                        "nullable": true,
                        "description": "The product's page in this shop, as `https://<shop hostname>/document/<legacy product id>`.\nNote the path uses the product's **legacy integer id**, not its UUID.\n\n`null` when there is no public page to link to, which is the case whenever any of these\nis true: the realm does not run storefronts at all, the product is not in *any*\nstorefront, or the product's `status` is not `published`. `null` is therefore expected\nfor unpublished titles, for titles in no shop, and for every title in a realm that does\nnot run shops.\n\n**It is not scoped strictly to this record.** If the product is not in *this* shop but\nis in another, the field falls back to the other shop's URL — the realm's main\nstorefront if the product is in it, otherwise the first one it is in. A disabled entry\ncan therefore still carry a working URL pointing somewhere else, and a URL is only\nreliably this record's shop when `storefront.name` matches its hostname.\n\nThe hostname is resolved for the environment answering the request, so a call against\na test system returns that system's host rather than the production one.\n",
                        "example": "https://www.example.com/document/14937"
                    },
                    "enabled": {
                        "type": "boolean",
                        "description": "Whether the product is offered in this storefront. This is the switch the whole record\nturns on: when it is `false` the shop does not list or sell the title, and the response\nreports `preview_percentage` as `0`, `free_of_charge` as `false` and the three `html_*`\nfields as `null`, whatever values the product itself carries.\n\nIt is `false` on placeholder entries for shops the product was never added to as well,\nso `enabled: false` alone does not distinguish \"withdrawn from this shop\" from \"never\nadded to it\" — `_id` is what distinguishes those.\n\nBecause the collection returns one entry per shop in the realm rather than one per shop\nthe title is in, the length of `items` is the number of shops the realm runs. Counting\nentries with `enabled: true` gives the number the title is in, and `enabled` is the\nfield to test for \"offered here\".\n\nThis is the one field a client can meaningfully change. A `PUT` of\n`{\"enabled\": false}` withdraws a title from one shop while leaving it in the others.\nIt does not delete the record, and there is no `DELETE` on this resource — the record\nsurvives, so switching the title back on later is another `PUT` rather than a fresh\n`POST`, and `_id` does not change.\n",
                        "example": true
                    },
                    "preview_percentage": {
                        "type": "integer",
                        "description": "How much of the book a visitor may read for free in this shop, as a whole-number\npercentage of its pages, `0` to `100`. This drives the reading sample the shop\nrenders — `25` means the first quarter of the text is open, the rest is behind the\npurchase.\n\nThe value is **per product, not per shop**: the same value is returned for every shop\nof a product and cannot differ between them, so every enabled entry in the list shows\nthe same number, and only the `0` reported on disabled entries differs. `0` on an\nenabled entry means no free reading sample at all.\n\n`100` is the special case: the entire text is readable without paying, which is what\nmakes `free_of_charge` `true`.\n\nRead-only here — this endpoint's `PUT` does not accept it.\n",
                        "example": 25
                    },
                    "html_title": {
                        "type": "string",
                        "nullable": true,
                        "description": "Override for the `<title>` element of the shop's product page. `null` means no override\nhas been set and the shop composes the title itself from the product metadata.\n\nThe same value is returned for every shop of a product and cannot differ between them;\ndisabled entries report `null`. Read-only through this endpoint.\n",
                        "example": null
                    },
                    "html_meta_description": {
                        "type": "string",
                        "nullable": true,
                        "description": "The `<meta name=\"description\">` of the shop's product page — the snippet a search\nengine shows under the link. Plain text, no markup, and in practice kept near the\n155 characters a search engine displays — the same length the default derived from the\nproduct's abstract is truncated to when no value has been set.\n\n`null` means no value has been set and the shop falls back to its own derivation.\n\nLike `html_title` and `html_meta_keywords`, this value is **shared, not per-shop**: the\nsame values are returned for every shop of a product and cannot differ between them, so\nevery enabled entry in the collection repeats the same three strings and one shop cannot\nbe given a different meta description from another. Disabled entries report `null` for\nall three. All three are also **read-only through this endpoint**: `PUT` and `POST` read\nonly `storefront` and `enabled`, and silently discard everything else.\n",
                        "example": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag"
                    },
                    "html_meta_keywords": {
                        "type": "string",
                        "nullable": true,
                        "description": "The `<meta name=\"keywords\">` of the shop's product page: a single string of\ncomma-separated terms, **not** a JSON array. The convention is a bare `,` with no space\nafter it, matching the default derived from the product's search tags when no value has\nbeen set.\n\nModern search engines ignore this element; it survives for shop-internal search and\nfor feeds that still expect it. `null` means no value has been set. The same value is\nreturned for every shop of a product and cannot differ between them; disabled entries\nreport `null`. Read-only through this endpoint.\n",
                        "example": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag"
                    },
                    "free_of_charge": {
                        "type": "boolean",
                        "description": "Whether a visitor can get the whole work in this shop without paying. Derived rather\nthan set, and computed as follows:\n\n- `true` when `preview_percentage` is `100` — the complete text is in the free reading\n  sample, so there is nothing left to buy. This is the intended meaning.\n- `true` otherwise when the product has **no gross e-book price** in EUR for Germany on\n  today's date. A title with no priced e-book edition therefore reports\n  `free_of_charge: true` even though it is not on offer at all, and a title priced only\n  outside the euro area does the same.\n- `false` whenever `enabled` is `false`, without either rule being evaluated.\n\nThe field therefore means \"this shop cannot charge for the e-book\" rather than \"this is\na giveaway\", and the product's prices are the authority on which of the two situations\na `true` represents.\n",
                        "example": false
                    }
                },
                "required": []
            }
        }
    },
    "required": [
        "items"
    ]
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "required": [
        "errors"
    ],
    "properties": {
        "errors": {
            "type": "array",
            "description": "List of errors occurred",
            "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "_id",
                    "message",
                    "stack_trace",
                    "vars"
                ],
                "properties": {
                    "_id": {
                        "type": "string",
                        "description": "The identifier of the error"
                    },
                    "stack_trace": {
                        "type": "array",
                        "description": "A stacktrace of the error occurred. Only for testing systems.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "message": {
                        "type": "string",
                        "description": "A translated message of the error"
                    },
                    "vars": {
                        "type": "object",
                        "description": "A dict containing additional values of the context of the error."
                    }
                }
            }
        }
    }
}

POST /resource/v4/products/{product_id}/storefronts

Add a product to a storefront

Description

Creates the product-to-storefront record that the collection endpoint shows as a placeholder, and returns it in full.

The body needs storefront._id — the UUID of a shop in the same realm as the product — and enabled. Those are the only two fields read; everything else in the body is ignored. Nothing else about the body is validated, and omitting storefront is not an error: a POST without it creates a record with no shop attached — one that will never appear in the collection, because the collection is driven by the realm's shops rather than by the records that exist. storefront._id belongs on every POST.

The reply is 201 with the complete record, including the derived url, preview_percentage, free_of_charge and the product-level html_* values.

Input parameters

Path parameters

Parameter Type Default Nullable Description
product_id string No UUID (or legacy integer id) of the product.

Request body

{
    "storefront": {
        "_id": "9ee62740-e617-11ee-a221-00505699575c"
    },
    "enabled": true
}
Schema of the request body
{
    "type": "object",
    "additionalProperties": true,
    "properties": {
        "storefront": {
            "type": "object",
            "additionalProperties": true,
            "description": "Which shop to add the product to. Only `_id` is read; a `name` sent alongside it is ignored. The shop must belong to the same realm as the product.",
            "properties": {
                "_id": {
                    "type": "string",
                    "description": "UUID of the storefront, as returned by `/resource/v4/storefronts`.",
                    "example": "9ee62740-e617-11ee-a221-00505699575c"
                }
            }
        },
        "enabled": {
            "type": "boolean",
            "description": "Whether the product is offered in this shop. Defaults to enabled when omitted.",
            "example": true
        }
    }
}

Responses

{
    "_id": "d1a7c4b0-6011-11ef-a221-00505699575c",
    "enabled": true,
    "free_of_charge": false,
    "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
    "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
    "html_title": null,
    "preview_percentage": 25,
    "storefront": {
        "_id": "9ee62740-e617-11ee-a221-00505699575c",
        "_links": {
            "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee62740-e617-11ee-a221-00505699575c"
        },
        "name": "zeilenquarz-fachbuch"
    },
    "url": "https://www.example.com/document/14937"
}
Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "description": "One product's presence in one storefront — the link between a product and a shop.",
    "example": {
        "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
        "storefront": {
            "_id": "9ee61a17-e617-11ee-a221-00505699575c",
            "_links": {
                "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
            },
            "name": "zeilenquarz"
        },
        "url": "https://www.example.com/document/14937",
        "enabled": true,
        "preview_percentage": 25,
        "html_title": null,
        "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
        "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
        "free_of_charge": false
    },
    "properties": {
        "_id": {
            "type": "string",
            "description": "UUID of this product-to-storefront link. It identifies the link, not the shop — the\nshop's own id is `storefront._id`, and passing that one to the single-record endpoint\nis a `404`.\n\n**The empty string** marks a placeholder — one of the entries the collection returns\nfor a shop the product was never added to. `\"\"` means \"not in this shop\", and such an\nentry is not addressable: appending the empty string to the single-record path is a\n`404`, so the record has to be created with `POST` first.\n\nThe single-record path accepts this UUID and nothing else; a legacy integer id does not\nresolve there. Storefront links carry no `_legacy_id` in any case: this resource\nidentified itself by UUID already in v3, so v3 and v4 return the same `_id`. Records\nhere also carry neither a `_version` nor a record-level `_links`, both of which most v4\nresources have.\n",
            "example": "b62d8555-5fa4-11ef-a221-00505699575c"
        },
        "storefront": {
            "type": "object",
            "additionalProperties": false,
            "description": "The shop this record is about — the same representation the top-level\n`/resource/v4/storefronts/{id}` endpoint returns. Realms typically run several:\na main shop plus specialised ones for particular subject areas or product forms.\n\n`{\"storefront\": {\"_id\": \"<uuid>\"}}` is what says which shop a record is for: on `POST`\nit attaches the new record to a shop, and on `PUT` it moves an existing record to\nanother shop of the same realm. Only `_id` is read; a `name` sent alongside it is\nignored. The shop must belong to the same realm as the product, and one that does not —\nlike one that does not exist at all — is rejected with `400 Bad Request`.\n\n`storefront` and `enabled` are the only two fields `PUT` and `POST` read from the body.\nEverything else sent is discarded without comment.\n",
            "properties": {
                "_id": {
                    "type": "string",
                    "description": "UUID of the storefront. This is the id to use against `/resource/v4/storefronts/{id}`, and the one to send when creating a record.",
                    "example": "9ee61a17-e617-11ee-a221-00505699575c"
                },
                "_links": {
                    "type": "object",
                    "description": "`self` — the storefront's own resource, `/resource/v4/storefronts/{storefront_id}`."
                },
                "name": {
                    "type": "string",
                    "description": "The shop's short name: a lowercase slug, unique within the realm, used as the\nstorefront's handle throughout the platform rather than as a display title. It\nis normally the first component of the shop's hostname, which is why the `url`\nof an entry usually echoes it.\n",
                    "example": "zeilenquarz"
                }
            }
        },
        "url": {
            "type": "string",
            "nullable": true,
            "description": "The product's page in this shop, as `https://<shop hostname>/document/<legacy product id>`.\nNote the path uses the product's **legacy integer id**, not its UUID.\n\n`null` when there is no public page to link to, which is the case whenever any of these\nis true: the realm does not run storefronts at all, the product is not in *any*\nstorefront, or the product's `status` is not `published`. `null` is therefore expected\nfor unpublished titles, for titles in no shop, and for every title in a realm that does\nnot run shops.\n\n**It is not scoped strictly to this record.** If the product is not in *this* shop but\nis in another, the field falls back to the other shop's URL — the realm's main\nstorefront if the product is in it, otherwise the first one it is in. A disabled entry\ncan therefore still carry a working URL pointing somewhere else, and a URL is only\nreliably this record's shop when `storefront.name` matches its hostname.\n\nThe hostname is resolved for the environment answering the request, so a call against\na test system returns that system's host rather than the production one.\n",
            "example": "https://www.example.com/document/14937"
        },
        "enabled": {
            "type": "boolean",
            "description": "Whether the product is offered in this storefront. This is the switch the whole record\nturns on: when it is `false` the shop does not list or sell the title, and the response\nreports `preview_percentage` as `0`, `free_of_charge` as `false` and the three `html_*`\nfields as `null`, whatever values the product itself carries.\n\nIt is `false` on placeholder entries for shops the product was never added to as well,\nso `enabled: false` alone does not distinguish \"withdrawn from this shop\" from \"never\nadded to it\" — `_id` is what distinguishes those.\n\nBecause the collection returns one entry per shop in the realm rather than one per shop\nthe title is in, the length of `items` is the number of shops the realm runs. Counting\nentries with `enabled: true` gives the number the title is in, and `enabled` is the\nfield to test for \"offered here\".\n\nThis is the one field a client can meaningfully change. A `PUT` of\n`{\"enabled\": false}` withdraws a title from one shop while leaving it in the others.\nIt does not delete the record, and there is no `DELETE` on this resource — the record\nsurvives, so switching the title back on later is another `PUT` rather than a fresh\n`POST`, and `_id` does not change.\n",
            "example": true
        },
        "preview_percentage": {
            "type": "integer",
            "description": "How much of the book a visitor may read for free in this shop, as a whole-number\npercentage of its pages, `0` to `100`. This drives the reading sample the shop\nrenders — `25` means the first quarter of the text is open, the rest is behind the\npurchase.\n\nThe value is **per product, not per shop**: the same value is returned for every shop\nof a product and cannot differ between them, so every enabled entry in the list shows\nthe same number, and only the `0` reported on disabled entries differs. `0` on an\nenabled entry means no free reading sample at all.\n\n`100` is the special case: the entire text is readable without paying, which is what\nmakes `free_of_charge` `true`.\n\nRead-only here — this endpoint's `PUT` does not accept it.\n",
            "example": 25
        },
        "html_title": {
            "type": "string",
            "nullable": true,
            "description": "Override for the `<title>` element of the shop's product page. `null` means no override\nhas been set and the shop composes the title itself from the product metadata.\n\nThe same value is returned for every shop of a product and cannot differ between them;\ndisabled entries report `null`. Read-only through this endpoint.\n",
            "example": null
        },
        "html_meta_description": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"description\">` of the shop's product page — the snippet a search\nengine shows under the link. Plain text, no markup, and in practice kept near the\n155 characters a search engine displays — the same length the default derived from the\nproduct's abstract is truncated to when no value has been set.\n\n`null` means no value has been set and the shop falls back to its own derivation.\n\nLike `html_title` and `html_meta_keywords`, this value is **shared, not per-shop**: the\nsame values are returned for every shop of a product and cannot differ between them, so\nevery enabled entry in the collection repeats the same three strings and one shop cannot\nbe given a different meta description from another. Disabled entries report `null` for\nall three. All three are also **read-only through this endpoint**: `PUT` and `POST` read\nonly `storefront` and `enabled`, and silently discard everything else.\n",
            "example": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag"
        },
        "html_meta_keywords": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"keywords\">` of the shop's product page: a single string of\ncomma-separated terms, **not** a JSON array. The convention is a bare `,` with no space\nafter it, matching the default derived from the product's search tags when no value has\nbeen set.\n\nModern search engines ignore this element; it survives for shop-internal search and\nfor feeds that still expect it. `null` means no value has been set. The same value is\nreturned for every shop of a product and cannot differ between them; disabled entries\nreport `null`. Read-only through this endpoint.\n",
            "example": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag"
        },
        "free_of_charge": {
            "type": "boolean",
            "description": "Whether a visitor can get the whole work in this shop without paying. Derived rather\nthan set, and computed as follows:\n\n- `true` when `preview_percentage` is `100` — the complete text is in the free reading\n  sample, so there is nothing left to buy. This is the intended meaning.\n- `true` otherwise when the product has **no gross e-book price** in EUR for Germany on\n  today's date. A title with no priced e-book edition therefore reports\n  `free_of_charge: true` even though it is not on offer at all, and a title priced only\n  outside the euro area does the same.\n- `false` whenever `enabled` is `false`, without either rule being evaluated.\n\nThe field therefore means \"this shop cannot charge for the e-book\" rather than \"this is\na giveaway\", and the product's prices are the authority on which of the two situations\na `true` represents.\n",
            "example": false
        }
    },
    "required": []
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "required": [
        "errors"
    ],
    "properties": {
        "errors": {
            "type": "array",
            "description": "List of errors occurred",
            "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "_id",
                    "message",
                    "stack_trace",
                    "vars"
                ],
                "properties": {
                    "_id": {
                        "type": "string",
                        "description": "The identifier of the error"
                    },
                    "stack_trace": {
                        "type": "array",
                        "description": "A stacktrace of the error occurred. Only for testing systems.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "message": {
                        "type": "string",
                        "description": "A translated message of the error"
                    },
                    "vars": {
                        "type": "object",
                        "description": "A dict containing additional values of the context of the error."
                    }
                }
            }
        }
    }
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

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}/storefronts/{storefront_record_id}

Retrieve a single product storefront record

Description

Fetches one product-to-storefront record by the _id the collection endpoint returned.

That id belongs to the link, not to the shop. Passing storefront._id — the shop's own UUID — is a 404, and so is the empty _id of a placeholder entry, because no record exists behind it. The UUID is the only accepted form; a legacy integer id does not resolve here, and records on this resource carry no _legacy_id in any case.

Input parameters

Path parameters

Parameter Type Default Nullable Description
product_id string No UUID (or legacy integer id) of the product.
storefront_record_id string No UUID of the product-to-storefront record — the `_id` of a list entry, not `storefront._id`.

Responses

{
    "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
    "enabled": true,
    "free_of_charge": false,
    "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
    "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
    "html_title": null,
    "preview_percentage": 25,
    "storefront": {
        "_id": "9ee61a17-e617-11ee-a221-00505699575c",
        "_links": {
            "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
        },
        "name": "zeilenquarz"
    },
    "url": "https://www.example.com/document/14937"
}
Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "description": "One product's presence in one storefront — the link between a product and a shop.",
    "example": {
        "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
        "storefront": {
            "_id": "9ee61a17-e617-11ee-a221-00505699575c",
            "_links": {
                "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
            },
            "name": "zeilenquarz"
        },
        "url": "https://www.example.com/document/14937",
        "enabled": true,
        "preview_percentage": 25,
        "html_title": null,
        "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
        "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
        "free_of_charge": false
    },
    "properties": {
        "_id": {
            "type": "string",
            "description": "UUID of this product-to-storefront link. It identifies the link, not the shop — the\nshop's own id is `storefront._id`, and passing that one to the single-record endpoint\nis a `404`.\n\n**The empty string** marks a placeholder — one of the entries the collection returns\nfor a shop the product was never added to. `\"\"` means \"not in this shop\", and such an\nentry is not addressable: appending the empty string to the single-record path is a\n`404`, so the record has to be created with `POST` first.\n\nThe single-record path accepts this UUID and nothing else; a legacy integer id does not\nresolve there. Storefront links carry no `_legacy_id` in any case: this resource\nidentified itself by UUID already in v3, so v3 and v4 return the same `_id`. Records\nhere also carry neither a `_version` nor a record-level `_links`, both of which most v4\nresources have.\n",
            "example": "b62d8555-5fa4-11ef-a221-00505699575c"
        },
        "storefront": {
            "type": "object",
            "additionalProperties": false,
            "description": "The shop this record is about — the same representation the top-level\n`/resource/v4/storefronts/{id}` endpoint returns. Realms typically run several:\na main shop plus specialised ones for particular subject areas or product forms.\n\n`{\"storefront\": {\"_id\": \"<uuid>\"}}` is what says which shop a record is for: on `POST`\nit attaches the new record to a shop, and on `PUT` it moves an existing record to\nanother shop of the same realm. Only `_id` is read; a `name` sent alongside it is\nignored. The shop must belong to the same realm as the product, and one that does not —\nlike one that does not exist at all — is rejected with `400 Bad Request`.\n\n`storefront` and `enabled` are the only two fields `PUT` and `POST` read from the body.\nEverything else sent is discarded without comment.\n",
            "properties": {
                "_id": {
                    "type": "string",
                    "description": "UUID of the storefront. This is the id to use against `/resource/v4/storefronts/{id}`, and the one to send when creating a record.",
                    "example": "9ee61a17-e617-11ee-a221-00505699575c"
                },
                "_links": {
                    "type": "object",
                    "description": "`self` — the storefront's own resource, `/resource/v4/storefronts/{storefront_id}`."
                },
                "name": {
                    "type": "string",
                    "description": "The shop's short name: a lowercase slug, unique within the realm, used as the\nstorefront's handle throughout the platform rather than as a display title. It\nis normally the first component of the shop's hostname, which is why the `url`\nof an entry usually echoes it.\n",
                    "example": "zeilenquarz"
                }
            }
        },
        "url": {
            "type": "string",
            "nullable": true,
            "description": "The product's page in this shop, as `https://<shop hostname>/document/<legacy product id>`.\nNote the path uses the product's **legacy integer id**, not its UUID.\n\n`null` when there is no public page to link to, which is the case whenever any of these\nis true: the realm does not run storefronts at all, the product is not in *any*\nstorefront, or the product's `status` is not `published`. `null` is therefore expected\nfor unpublished titles, for titles in no shop, and for every title in a realm that does\nnot run shops.\n\n**It is not scoped strictly to this record.** If the product is not in *this* shop but\nis in another, the field falls back to the other shop's URL — the realm's main\nstorefront if the product is in it, otherwise the first one it is in. A disabled entry\ncan therefore still carry a working URL pointing somewhere else, and a URL is only\nreliably this record's shop when `storefront.name` matches its hostname.\n\nThe hostname is resolved for the environment answering the request, so a call against\na test system returns that system's host rather than the production one.\n",
            "example": "https://www.example.com/document/14937"
        },
        "enabled": {
            "type": "boolean",
            "description": "Whether the product is offered in this storefront. This is the switch the whole record\nturns on: when it is `false` the shop does not list or sell the title, and the response\nreports `preview_percentage` as `0`, `free_of_charge` as `false` and the three `html_*`\nfields as `null`, whatever values the product itself carries.\n\nIt is `false` on placeholder entries for shops the product was never added to as well,\nso `enabled: false` alone does not distinguish \"withdrawn from this shop\" from \"never\nadded to it\" — `_id` is what distinguishes those.\n\nBecause the collection returns one entry per shop in the realm rather than one per shop\nthe title is in, the length of `items` is the number of shops the realm runs. Counting\nentries with `enabled: true` gives the number the title is in, and `enabled` is the\nfield to test for \"offered here\".\n\nThis is the one field a client can meaningfully change. A `PUT` of\n`{\"enabled\": false}` withdraws a title from one shop while leaving it in the others.\nIt does not delete the record, and there is no `DELETE` on this resource — the record\nsurvives, so switching the title back on later is another `PUT` rather than a fresh\n`POST`, and `_id` does not change.\n",
            "example": true
        },
        "preview_percentage": {
            "type": "integer",
            "description": "How much of the book a visitor may read for free in this shop, as a whole-number\npercentage of its pages, `0` to `100`. This drives the reading sample the shop\nrenders — `25` means the first quarter of the text is open, the rest is behind the\npurchase.\n\nThe value is **per product, not per shop**: the same value is returned for every shop\nof a product and cannot differ between them, so every enabled entry in the list shows\nthe same number, and only the `0` reported on disabled entries differs. `0` on an\nenabled entry means no free reading sample at all.\n\n`100` is the special case: the entire text is readable without paying, which is what\nmakes `free_of_charge` `true`.\n\nRead-only here — this endpoint's `PUT` does not accept it.\n",
            "example": 25
        },
        "html_title": {
            "type": "string",
            "nullable": true,
            "description": "Override for the `<title>` element of the shop's product page. `null` means no override\nhas been set and the shop composes the title itself from the product metadata.\n\nThe same value is returned for every shop of a product and cannot differ between them;\ndisabled entries report `null`. Read-only through this endpoint.\n",
            "example": null
        },
        "html_meta_description": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"description\">` of the shop's product page — the snippet a search\nengine shows under the link. Plain text, no markup, and in practice kept near the\n155 characters a search engine displays — the same length the default derived from the\nproduct's abstract is truncated to when no value has been set.\n\n`null` means no value has been set and the shop falls back to its own derivation.\n\nLike `html_title` and `html_meta_keywords`, this value is **shared, not per-shop**: the\nsame values are returned for every shop of a product and cannot differ between them, so\nevery enabled entry in the collection repeats the same three strings and one shop cannot\nbe given a different meta description from another. Disabled entries report `null` for\nall three. All three are also **read-only through this endpoint**: `PUT` and `POST` read\nonly `storefront` and `enabled`, and silently discard everything else.\n",
            "example": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag"
        },
        "html_meta_keywords": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"keywords\">` of the shop's product page: a single string of\ncomma-separated terms, **not** a JSON array. The convention is a bare `,` with no space\nafter it, matching the default derived from the product's search tags when no value has\nbeen set.\n\nModern search engines ignore this element; it survives for shop-internal search and\nfor feeds that still expect it. `null` means no value has been set. The same value is\nreturned for every shop of a product and cannot differ between them; disabled entries\nreport `null`. Read-only through this endpoint.\n",
            "example": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag"
        },
        "free_of_charge": {
            "type": "boolean",
            "description": "Whether a visitor can get the whole work in this shop without paying. Derived rather\nthan set, and computed as follows:\n\n- `true` when `preview_percentage` is `100` — the complete text is in the free reading\n  sample, so there is nothing left to buy. This is the intended meaning.\n- `true` otherwise when the product has **no gross e-book price** in EUR for Germany on\n  today's date. A title with no priced e-book edition therefore reports\n  `free_of_charge: true` even though it is not on offer at all, and a title priced only\n  outside the euro area does the same.\n- `false` whenever `enabled` is `false`, without either rule being evaluated.\n\nThe field therefore means \"this shop cannot charge for the e-book\" rather than \"this is\na giveaway\", and the product's prices are the authority on which of the two situations\na `true` represents.\n",
            "example": false
        }
    },
    "required": []
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "required": [
        "errors"
    ],
    "properties": {
        "errors": {
            "type": "array",
            "description": "List of errors occurred",
            "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "_id",
                    "message",
                    "stack_trace",
                    "vars"
                ],
                "properties": {
                    "_id": {
                        "type": "string",
                        "description": "The identifier of the error"
                    },
                    "stack_trace": {
                        "type": "array",
                        "description": "A stacktrace of the error occurred. Only for testing systems.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "message": {
                        "type": "string",
                        "description": "A translated message of the error"
                    },
                    "vars": {
                        "type": "object",
                        "description": "A dict containing additional values of the context of the error."
                    }
                }
            }
        }
    }
}

PUT /resource/v4/products/{product_id}/storefronts/{storefront_record_id}

Update a product storefront record

Description

A partial update: each field is applied only if the body contains it, so {"enabled": false} withdraws the product from the shop and leaves everything else untouched. Sending storefront._id moves the record to a different shop of the same realm.

Only those two fields are read. preview_percentage, url, free_of_charge and the three html_* fields are read-only on this resource and are ignored if sent.

{"enabled": false} is also the only way to take a title out of a shop: there is no DELETE on this resource. The record survives, so switching the title back on later is another PUT and _id does not change.

The reply is 201 Resource created, not 200, and carries the updated record. The example withdraws the title from the shop.

Input parameters

Path parameters

Parameter Type Default Nullable Description
product_id string No UUID (or legacy integer id) of the product.
storefront_record_id string No UUID of the product-to-storefront record.

Request body

{
    "enabled": false
}
Schema of the request body
{
    "type": "object",
    "additionalProperties": true,
    "properties": {
        "storefront": {
            "type": "object",
            "additionalProperties": true,
            "description": "Move the record to another shop of the same realm. Only `_id` is read. Omit it to leave the shop assignment unchanged.",
            "properties": {
                "_id": {
                    "type": "string",
                    "description": "UUID of the storefront.",
                    "example": "9ee62740-e617-11ee-a221-00505699575c"
                }
            }
        },
        "enabled": {
            "type": "boolean",
            "description": "Offer the product in this shop, or withdraw it. Omit to leave unchanged. Setting it to `false` is how a title is removed from a shop; there is no `DELETE` on this resource.",
            "example": false
        }
    }
}

Responses

{
    "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
    "enabled": false,
    "free_of_charge": false,
    "html_meta_description": null,
    "html_meta_keywords": null,
    "html_title": null,
    "preview_percentage": 0,
    "storefront": {
        "_id": "9ee61a17-e617-11ee-a221-00505699575c",
        "_links": {
            "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
        },
        "name": "zeilenquarz"
    },
    "url": "https://www.example.com/document/14937"
}
Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "description": "One product's presence in one storefront — the link between a product and a shop.",
    "example": {
        "_id": "b62d8555-5fa4-11ef-a221-00505699575c",
        "storefront": {
            "_id": "9ee61a17-e617-11ee-a221-00505699575c",
            "_links": {
                "self": "https://api.openpublishing.com/resource/v4/storefronts/9ee61a17-e617-11ee-a221-00505699575c"
            },
            "name": "zeilenquarz"
        },
        "url": "https://www.example.com/document/14937",
        "enabled": true,
        "preview_percentage": 25,
        "html_title": null,
        "html_meta_description": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag",
        "html_meta_keywords": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag",
        "free_of_charge": false
    },
    "properties": {
        "_id": {
            "type": "string",
            "description": "UUID of this product-to-storefront link. It identifies the link, not the shop — the\nshop's own id is `storefront._id`, and passing that one to the single-record endpoint\nis a `404`.\n\n**The empty string** marks a placeholder — one of the entries the collection returns\nfor a shop the product was never added to. `\"\"` means \"not in this shop\", and such an\nentry is not addressable: appending the empty string to the single-record path is a\n`404`, so the record has to be created with `POST` first.\n\nThe single-record path accepts this UUID and nothing else; a legacy integer id does not\nresolve there. Storefront links carry no `_legacy_id` in any case: this resource\nidentified itself by UUID already in v3, so v3 and v4 return the same `_id`. Records\nhere also carry neither a `_version` nor a record-level `_links`, both of which most v4\nresources have.\n",
            "example": "b62d8555-5fa4-11ef-a221-00505699575c"
        },
        "storefront": {
            "type": "object",
            "additionalProperties": false,
            "description": "The shop this record is about — the same representation the top-level\n`/resource/v4/storefronts/{id}` endpoint returns. Realms typically run several:\na main shop plus specialised ones for particular subject areas or product forms.\n\n`{\"storefront\": {\"_id\": \"<uuid>\"}}` is what says which shop a record is for: on `POST`\nit attaches the new record to a shop, and on `PUT` it moves an existing record to\nanother shop of the same realm. Only `_id` is read; a `name` sent alongside it is\nignored. The shop must belong to the same realm as the product, and one that does not —\nlike one that does not exist at all — is rejected with `400 Bad Request`.\n\n`storefront` and `enabled` are the only two fields `PUT` and `POST` read from the body.\nEverything else sent is discarded without comment.\n",
            "properties": {
                "_id": {
                    "type": "string",
                    "description": "UUID of the storefront. This is the id to use against `/resource/v4/storefronts/{id}`, and the one to send when creating a record.",
                    "example": "9ee61a17-e617-11ee-a221-00505699575c"
                },
                "_links": {
                    "type": "object",
                    "description": "`self` — the storefront's own resource, `/resource/v4/storefronts/{storefront_id}`."
                },
                "name": {
                    "type": "string",
                    "description": "The shop's short name: a lowercase slug, unique within the realm, used as the\nstorefront's handle throughout the platform rather than as a display title. It\nis normally the first component of the shop's hostname, which is why the `url`\nof an entry usually echoes it.\n",
                    "example": "zeilenquarz"
                }
            }
        },
        "url": {
            "type": "string",
            "nullable": true,
            "description": "The product's page in this shop, as `https://<shop hostname>/document/<legacy product id>`.\nNote the path uses the product's **legacy integer id**, not its UUID.\n\n`null` when there is no public page to link to, which is the case whenever any of these\nis true: the realm does not run storefronts at all, the product is not in *any*\nstorefront, or the product's `status` is not `published`. `null` is therefore expected\nfor unpublished titles, for titles in no shop, and for every title in a realm that does\nnot run shops.\n\n**It is not scoped strictly to this record.** If the product is not in *this* shop but\nis in another, the field falls back to the other shop's URL — the realm's main\nstorefront if the product is in it, otherwise the first one it is in. A disabled entry\ncan therefore still carry a working URL pointing somewhere else, and a URL is only\nreliably this record's shop when `storefront.name` matches its hostname.\n\nThe hostname is resolved for the environment answering the request, so a call against\na test system returns that system's host rather than the production one.\n",
            "example": "https://www.example.com/document/14937"
        },
        "enabled": {
            "type": "boolean",
            "description": "Whether the product is offered in this storefront. This is the switch the whole record\nturns on: when it is `false` the shop does not list or sell the title, and the response\nreports `preview_percentage` as `0`, `free_of_charge` as `false` and the three `html_*`\nfields as `null`, whatever values the product itself carries.\n\nIt is `false` on placeholder entries for shops the product was never added to as well,\nso `enabled: false` alone does not distinguish \"withdrawn from this shop\" from \"never\nadded to it\" — `_id` is what distinguishes those.\n\nBecause the collection returns one entry per shop in the realm rather than one per shop\nthe title is in, the length of `items` is the number of shops the realm runs. Counting\nentries with `enabled: true` gives the number the title is in, and `enabled` is the\nfield to test for \"offered here\".\n\nThis is the one field a client can meaningfully change. A `PUT` of\n`{\"enabled\": false}` withdraws a title from one shop while leaving it in the others.\nIt does not delete the record, and there is no `DELETE` on this resource — the record\nsurvives, so switching the title back on later is another `PUT` rather than a fresh\n`POST`, and `_id` does not change.\n",
            "example": true
        },
        "preview_percentage": {
            "type": "integer",
            "description": "How much of the book a visitor may read for free in this shop, as a whole-number\npercentage of its pages, `0` to `100`. This drives the reading sample the shop\nrenders — `25` means the first quarter of the text is open, the rest is behind the\npurchase.\n\nThe value is **per product, not per shop**: the same value is returned for every shop\nof a product and cannot differ between them, so every enabled entry in the list shows\nthe same number, and only the `0` reported on disabled entries differs. `0` on an\nenabled entry means no free reading sample at all.\n\n`100` is the special case: the entire text is readable without paying, which is what\nmakes `free_of_charge` `true`.\n\nRead-only here — this endpoint's `PUT` does not accept it.\n",
            "example": 25
        },
        "html_title": {
            "type": "string",
            "nullable": true,
            "description": "Override for the `<title>` element of the shop's product page. `null` means no override\nhas been set and the shop composes the title itself from the product metadata.\n\nThe same value is returned for every shop of a product and cannot differ between them;\ndisabled entries report `null`. Read-only through this endpoint.\n",
            "example": null
        },
        "html_meta_description": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"description\">` of the shop's product page — the snippet a search\nengine shows under the link. Plain text, no markup, and in practice kept near the\n155 characters a search engine displays — the same length the default derived from the\nproduct's abstract is truncated to when no value has been set.\n\n`null` means no value has been set and the shop falls back to its own derivation.\n\nLike `html_title` and `html_meta_keywords`, this value is **shared, not per-shop**: the\nsame values are returned for every shop of a product and cannot differ between them, so\nevery enabled entry in the collection repeats the same three strings and one shop cannot\nbe given a different meta description from another. Disabled entries report `null` for\nall three. All three are also **read-only through this endpoint**: `PUT` and `POST` read\nonly `storefront` and `enabled`, and silently discard everything else.\n",
            "example": "Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag"
        },
        "html_meta_keywords": {
            "type": "string",
            "nullable": true,
            "description": "The `<meta name=\"keywords\">` of the shop's product page: a single string of\ncomma-separated terms, **not** a JSON array. The convention is a bare `,` with no space\nafter it, matching the default derived from the product's search tags when no value has\nbeen set.\n\nModern search engines ignore this element; it survives for shop-internal search and\nfor feeds that still expect it. `null` means no value has been set. The same value is\nreturned for every shop of a product and cannot differ between them; disabled entries\nreport `null`. Read-only through this endpoint.\n",
            "example": "Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag"
        },
        "free_of_charge": {
            "type": "boolean",
            "description": "Whether a visitor can get the whole work in this shop without paying. Derived rather\nthan set, and computed as follows:\n\n- `true` when `preview_percentage` is `100` — the complete text is in the free reading\n  sample, so there is nothing left to buy. This is the intended meaning.\n- `true` otherwise when the product has **no gross e-book price** in EUR for Germany on\n  today's date. A title with no priced e-book edition therefore reports\n  `free_of_charge: true` even though it is not on offer at all, and a title priced only\n  outside the euro area does the same.\n- `false` whenever `enabled` is `false`, without either rule being evaluated.\n\nThe field therefore means \"this shop cannot charge for the e-book\" rather than \"this is\na giveaway\", and the product's prices are the authority on which of the two situations\na `true` represents.\n",
            "example": false
        }
    },
    "required": []
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

Schema of the response body
{
    "type": "object",
    "additionalProperties": false,
    "required": [
        "errors"
    ],
    "properties": {
        "errors": {
            "type": "array",
            "description": "List of errors occurred",
            "items": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                    "_id",
                    "message",
                    "stack_trace",
                    "vars"
                ],
                "properties": {
                    "_id": {
                        "type": "string",
                        "description": "The identifier of the error"
                    },
                    "stack_trace": {
                        "type": "array",
                        "description": "A stacktrace of the error occurred. Only for testing systems.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "message": {
                        "type": "string",
                        "description": "A translated message of the error"
                    },
                    "vars": {
                        "type": "object",
                        "description": "A dict containing additional values of the context of the error."
                    }
                }
            }
        }
    }
}

{
    "errors": [
        {
            "_id": "string",
            "stack_trace": [
                "string"
            ],
            "message": "string",
            "vars": {}
        }
    ]
}
⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information.

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 Storefront

Name Type Description Example
_id string

UUID of this product-to-storefront link. It identifies the link, not the shop — the shop's own id is storefront._id, and passing that one to the single-record endpoint is a 404.

The empty string marks a placeholder — one of the entries the collection returns for a shop the product was never added to. "" means "not in this shop", and such an entry is not addressable: appending the empty string to the single-record path is a 404, so the record has to be created with POST first.

The single-record path accepts this UUID and nothing else; a legacy integer id does not resolve there. Storefront links carry no _legacy_id in any case: this resource identified itself by UUID already in v3, so v3 and v4 return the same _id. Records here also carry neither a _version nor a record-level _links, both of which most v4 resources have.

b62d8555-5fa4-11ef-a221-00505699575c
enabled boolean

Whether the product is offered in this storefront. This is the switch the whole record turns on: when it is false the shop does not list or sell the title, and the response reports preview_percentage as 0, free_of_charge as false and the three html_* fields as null, whatever values the product itself carries.

It is false on placeholder entries for shops the product was never added to as well, so enabled: false alone does not distinguish "withdrawn from this shop" from "never added to it" — _id is what distinguishes those.

Because the collection returns one entry per shop in the realm rather than one per shop the title is in, the length of items is the number of shops the realm runs. Counting entries with enabled: true gives the number the title is in, and enabled is the field to test for "offered here".

This is the one field a client can meaningfully change. A PUT of {"enabled": false} withdraws a title from one shop while leaving it in the others. It does not delete the record, and there is no DELETE on this resource — the record survives, so switching the title back on later is another PUT rather than a fresh POST, and _id does not change.

true
free_of_charge boolean

Whether a visitor can get the whole work in this shop without paying. Derived rather than set, and computed as follows:

  • true when preview_percentage is 100 — the complete text is in the free reading sample, so there is nothing left to buy. This is the intended meaning.
  • true otherwise when the product has no gross e-book price in EUR for Germany on today's date. A title with no priced e-book edition therefore reports free_of_charge: true even though it is not on offer at all, and a title priced only outside the euro area does the same.
  • false whenever enabled is false, without either rule being evaluated.

The field therefore means "this shop cannot charge for the e-book" rather than "this is a giveaway", and the product's prices are the authority on which of the two situations a true represents.

false
html_meta_description string| null

The <meta name="description"> of the shop's product page — the snippet a search engine shows under the link. Plain text, no markup, and in practice kept near the 155 characters a search engine displays — the same length the default derived from the product's abstract is truncated to when no value has been set.

null means no value has been set and the shop falls back to its own derivation.

Like html_title and html_meta_keywords, this value is shared, not per-shop: the same values are returned for every shop of a product and cannot differ between them, so every enabled entry in the collection repeats the same three strings and one shop cannot be given a different meta description from another. Disabled entries report null for all three. All three are also read-only through this endpoint: PUT and POST read only storefront and enabled, and silently discard everything else.

Der unsignierte Teppich - Roman - Marit Sallenberg - E-Book 14,99 EUR - Zeilenquarz Verlag
html_meta_keywords string| null

The <meta name="keywords"> of the shop's product page: a single string of comma-separated terms, not a JSON array. The convention is a bare , with no space after it, matching the default derived from the product's search tags when no value has been set.

Modern search engines ignore this element; it survives for shop-internal search and for feeds that still expect it. null means no value has been set. The same value is returned for every shop of a product and cannot differ between them; disabled entries report null. Read-only through this endpoint.

Roman,Gegenwartsliteratur,Marit Sallenberg,Zeilenquarz Verlag
html_title string| null

Override for the <title> element of the shop's product page. null means no override has been set and the shop composes the title itself from the product metadata.

The same value is returned for every shop of a product and cannot differ between them; disabled entries report null. Read-only through this endpoint.

preview_percentage integer

How much of the book a visitor may read for free in this shop, as a whole-number percentage of its pages, 0 to 100. This drives the reading sample the shop renders — 25 means the first quarter of the text is open, the rest is behind the purchase.

The value is per product, not per shop: the same value is returned for every shop of a product and cannot differ between them, so every enabled entry in the list shows the same number, and only the 0 reported on disabled entries differs. 0 on an enabled entry means no free reading sample at all.

100 is the special case: the entire text is readable without paying, which is what makes free_of_charge true.

Read-only here — this endpoint's PUT does not accept it.

25
storefront Properties: _id, _links, name

The shop this record is about — the same representation the top-level /resource/v4/storefronts/{id} endpoint returns. Realms typically run several: a main shop plus specialised ones for particular subject areas or product forms.

{"storefront": {"_id": "<uuid>"}} is what says which shop a record is for: on POST it attaches the new record to a shop, and on PUT it moves an existing record to another shop of the same realm. Only _id is read; a name sent alongside it is ignored. The shop must belong to the same realm as the product, and one that does not — like one that does not exist at all — is rejected with 400 Bad Request.

storefront and enabled are the only two fields PUT and POST read from the body. Everything else sent is discarded without comment.

storefront._id string

UUID of the storefront. This is the id to use against /resource/v4/storefronts/{id}, and the one to send when creating a record.

9ee61a17-e617-11ee-a221-00505699575c
storefront._links object

self — the storefront's own resource, /resource/v4/storefronts/{storefront_id}.

storefront.name string

The shop's short name: a lowercase slug, unique within the realm, used as the storefront's handle throughout the platform rather than as a display title. It is normally the first component of the shop's hostname, which is why the url of an entry usually echoes it.

zeilenquarz
url string| null

The product's page in this shop, as https://<shop hostname>/document/<legacy product id>. Note the path uses the product's legacy integer id, not its UUID.

null when there is no public page to link to, which is the case whenever any of these is true: the realm does not run storefronts at all, the product is not in any storefront, or the product's status is not published. null is therefore expected for unpublished titles, for titles in no shop, and for every title in a realm that does not run shops.

It is not scoped strictly to this record. If the product is not in this shop but is in another, the field falls back to the other shop's URL — the realm's main storefront if the product is in it, otherwise the first one it is in. A disabled entry can therefore still carry a working URL pointing somewhere else, and a URL is only reliably this record's shop when storefront.name matches its hostname.

The hostname is resolved for the environment answering the request, so a call against a test system returns that system's host rather than the production one.

https://www.example.com/document/14937