Response Contract
A frontend reads every v2 response with the same assumptions. These rules are those assumptions; a response schema that breaks one breaks a consumer that never looked at the module. The fields of each endpoint are in the CMS Admin API and Storefront API references.
Null Handling
Section titled “Null Handling”A value that is inactive, empty or missing is null. The field is never omitted, never an empty string, and never
0 or false standing in for “no value”.
// Correct: the field is present, the value is null{"price": null, "special_price": null, "description": null}
// Wrong: the field is omitted{"description": "..."}
// Wrong: an empty string or a zero as "no value"{"price": "", "description": ""}{"price": "0.00"}The frontend can write if (product.price) everywhere, and never has to guess whether a missing field means
“not applicable” or “forgotten”. It follows that a response schema declares every field on every response: the set
of fields is fixed, only the values change.
Empty Collections
Section titled “Empty Collections”- Arrays:
[], nevernull, never omitted. - Objects used as maps (attributes by SKU):
{}, nevernull. - Single objects (
price):nullwhen absent.
Field Naming
Section titled “Field Naming”snake_caseeverywhere.- Booleans:
is_orhas_prefix (is_active,has_special_price). - References:
_idxfor business identifiers,_idfor database primary keys. Do not expose a primary key when an idx exists. - Translations:
_t9nfor a JSON dict of translations (name_t9n).
Pagination
Section titled “Pagination”One endpoint has one envelope. Pick it by the cost of counting.
Counted, for admin lists and small datasets. DRF’s page-number pagination, through the module’s
AdminPageNumberPagination:
{ "count": 142, "next": "https://.../?page=2", "previous": null, "results": []}Uncounted, for storefront lists where COUNT(*) is expensive (a large catalog, complex filters). The view
fetches one row more than the page size to learn whether a next page exists:
{ "has_next_page": true, "page": 2, "page_size": 24, "results": []}In both, results is always an array; an envelope may add keys of its own next to these. An endpoint with the uncounted envelope that needs a total offers it as a
separate {resource}/count/ endpoint, cached on its own.
- A monetary value is a decimal string (
"349.00"), never a float. - A price carries both gross and net; the frontend’s configuration decides which one it shows first.
- A rate is a decimal string too (
"0.23"), never a display form ("23%"). - No price means
price: null, never an object of nulls.
A list of media is an array sorted by position, [] when empty. An image carries a source_set (one entry per
rendered width, each with its width and height) and never a single hard-coded URL, so the frontend picks the
size.
Dates and Times
Section titled “Dates and Times”ISO 8601 with a timezone: "2025-03-24T14:30:00Z". Never a naive datetime, never a Unix timestamp.
Language, Currency and Country Parameters
Section titled “Language, Currency and Country Parameters”Storefront endpoints that return localized or priced data take the same three query parameters:
| Param | Format | Example |
|---|---|---|
language | ISO 639-1, two letters | en, pl |
currency | ISO 4217, three letters | EUR, PLN |
country | ISO 3166-1 alpha-2 | PL, DE |
A missing or unknown value falls back to the channel’s default language, currency or country, when the channel has one. An unknown value alone is not a 400.
Errors
Section titled “Errors”An error has one shape on every v2 endpoint, and it never carries SQL, file paths or a stack trace: Error Handling.