API Conventions
Both APIs follow the same rules for the things every client has to get right. The examples are real answers of the
public demo backend, https://demo-api.entirius.com, trimmed where marked. Storefront reads need no
credential; admin reads use the read-only staff login viewer / viewer on POST /api/token/
(Demo Credentials).
Channels in the URL
Section titled “Channels in the URL”A sales channel is a path segment, channel_idx: /api/matrix/v2/<channel_idx>/products/ on the storefront API,
/api/pim/v2/admin/<channel_idx>/products/ on the admin API. Admin routes that are not about one channel have no
such segment. The translator routes call the same identifier shop_idx
(/api/pim-translator/v2/admin/<shop_idx>/…).
GET /api/pim/v2/admin/channels/ lists the channels with their idx, default language, default currency and
languages. The demo has three: sloik-pl (PLN; pl, en), sloik-eu (EUR; en, es, de, pl) and
sloik-firmy (PLN; pl, en); the seeded local stack of the Quickstart has default-europe and default-local
(Your First Request). An unknown channel answers 404 NOT_FOUND. What a channel is and how modules share it:
Channel Access Patterns.
Languages and Translations
Section titled “Languages and Translations”Admin payloads carry every language. A translatable field comes twice: the plain field in the channel’s default
language and a _t9n dictionary keyed by language code. One feature from
GET /api/pim/v2/admin/sloik-pl/features/?page_size=1 (sloik-pl defaults to Polish; the dictionary lists every
language of the installation, not only the channel’s two; cut: other fields):
{"idx": "name", "name": "Nazwa", "name_t9n": {"de": "Name", "en": "Name", "es": "Nombre", "pl": "Nazwa"}}Storefront answers carry one language. The language, currency and country query parameters pick it
(?language=pl¤cy=EUR); a missing or unknown value falls back to the channel’s default. The formats and the
fallback rule: Response Contract.
How languages are stored: Languages.
The same product list in three requests (page_size=2, only name and the gross price shown):
GET /api/matrix/v2/sloik-pl/products/?language=en SC-POD-011 Stars over the desert 119.99 PLNGET /api/matrix/v2/sloik-eu/products/?language=de¤cy=EUR SC-POD-011 Sterne über der Wüste 27.90 EURGET /api/matrix/v2/sloik-pl/products/?language=xx¤cy=ZZZ SC-POD-011 Gwiazdy nad pustynią 119.99 PLNThe last one is the fallback: unknown values answer in the channel’s defaults, with status 200.
Pagination
Section titled “Pagination”A paginated list takes page (from 1) and page_size, and answers in one of two envelopes.
Counted, on admin lists. GET /api/pim/v2/admin/sloik-pl/products/?page_size=3:
{ "count": 212, "next": "https://demo-api.entirius.com/api/pim/v2/admin/sloik-pl/products/?page=2&page_size=3", "previous": null, "results": []}Uncounted, on storefront catalog lists, where counting a filtered catalog costs more than the page itself.
GET /api/matrix/v2/sloik-pl/products/?page_size=2:
{ "has_next_page": true, "page": 1, "page_size": 2, "results": [], "meta": {"status": "ok", "warnings": []}}results is cut to [] in both examples. Follow next on a counted list; on an uncounted one ask for page + 1
while has_next_page is true. The envelope follows the kind of list, not the API: the storefront FAQ items
(GET /api/faq/v2/sloik-pl/items/) are counted.
The default and the largest page_size differ per endpoint, and the explorer states them on each operation. Two
examples: PIM admin lists default to 20 and stop at 100; the storefront product list defaults to 12 and stops at 100.
A larger page_size is cut to the maximum, not refused: ?page_size=500 on the storefront list answers
"page_size": 100.
Sorting
Section titled “Sorting”Admin lists take ordering with a field name, and a leading - for descending:
GET /api/pim/v2/admin/sloik-pl/products/?ordering=-sku answers SC-ZIM-010, SC-ZIM-009, SC-ZIM-008. The fields are per endpoint and named in the
explorer; the PIM product list knows sku, visibility, gap_worst_severity and gap_count. An unknown value is
not an error there: ?ordering=-name answers the list in ascending SKU order (SC-AKC-001, SC-AKC-002, …).
A few lists name the parameter sort instead, with the same - prefix, for example the FAQ items of the storefront
API.
The storefront product list sorts with s_{field} and the direction as the value:
GET /api/matrix/v2/sloik-pl/products/?s_price=asc. Fields are price, name, created, sku, on_stock,
relevance and any feature the channel marks as sortable (GET /api/matrix/v2/<channel_idx>/options/ lists them under
sort). Price sorts by the price the customer
pays: on the demo SC-AKC-012 has a gross price of 12.99 and a final price of 6.99, and sits between 5.99 and 6.99.
Filtering and Search
Section titled “Filtering and Search”search is the free-text parameter on both APIs:
GET /api/pim/v2/admin/sloik-pl/products/?search=zorza answers the 2 demo products that match (SC-POD-008,
SC-ZIM-009); the storefront list takes the same parameter. Which fields it reads is part of each operation’s
description.
The storefront product list filters on product features:
| Parameter | Does | Example |
|---|---|---|
q_{feature_idx} | value filter on a feature; repeat it for several values (OR within one feature) | ?q_<feature_idx>=<value> |
r_{feature_idx} | range filter, two values, wired for price | ?r_price=100&r_price=500 |
category, on_stock, product_type, sku, url_key | fixed filters | ?category=zorza&on_stock=false |
GET /api/matrix/v2/sloik-pl/products/count/ takes the same filters and answers only the number. On the demo:
?q_nastroj=spokoj -> {"count": 105, ...}?q_nastroj=spokoj&q_nastroj=radosc -> {"count": 160, ...}?r_price=100&r_price=125 -> {"count": 14, ...}GET /api/matrix/v2/<channel_idx>/options/ lists the feature indexes, their values and counts for the filter sidebar.
Every other filter belongs to one endpoint and is listed with it in the CMS Admin API and Storefront API explorers.
Errors
Section titled “Errors”The v2 error handler has one envelope. GET /api/pim/v2/admin/channels/ without a token answers 401:
{"error": "AUTHENTICATION_REQUIRED", "message": "Authentication credentials were not provided or are invalid.", "debug_id": "93ccc863", "details": []}A bad filter answers 400 with the field named. GET /api/agreements/v2/admin/clause-sets/?channel_idx=sloik-pl¤t=maybe
with the viewer token (location says body even though the value came in the query string):
{"error": "VALIDATION_ERROR", "message": "Request validation failed.", "debug_id": "43d73ce5", "details": [{"field": "current", "location": "body", "issue": "INVALID", "description": "Input should be a valid boolean, unable to interpret input"}]}An unknown channel, GET /api/matrix/v2/nope/products/, answers 404:
{"error": "NOT_FOUND", "message": "The requested resource was not found.", "debug_id": "aa247f93", "details": []}error is the code to branch on, message a fixed sentence per status, details a list of
field / location / issue / description entries (one per invalid field on a validation error), and debug_id
the id of the matching log line. Quote it when you report a problem.
The codes are VALIDATION_ERROR and INVALID_REQUEST (400), AUTHENTICATION_REQUIRED (401), PERMISSION_DENIED
(403), NOT_FOUND (404), CONFLICT or the conflict’s own code (409), RATE_LIMITED (429) and INTERNAL_ERROR
(500). The table with what raises each one: Error Handling.
Not every route emits it yet. Measured on the demo:
- Unknown admin object ids answer 404
{"detail": "Product with SKU 'NOPE-123' not found in channel 'sloik-pl'"}. GET /api/deliverypoints/v2/sloik-pl/points/nearby/?lat=abc&lng=1answers 400{"detail": "lat and lng must be valid numbers."}.- Storefront catalog operations wrap their answers: an unknown SKU on
/api/matrix/v2/sloik-pl/products/NOPE-123/answers 404{"meta": {"status": "NOT_FOUND", ...}, "data": "Product NOPE-123 does not exist"}.
Branch on the status code first, on error only when the body has it.
A 500 never explains itself. It answers INTERNAL_ERROR with a debug_id, or {"detail": "Internal server error [<error_id>]"} from endpoints that carry an id of their own.
Either id finds the cause in the server log.
Rate Limits
Section titled “Rate Limits”No limit applies to the API as a whole. Limits sit on the endpoints that create state without a login or that start
expensive work, each with a scope and a default rate. The modules are listed in the
Module Catalog. A refused request answers 429 with the error code
RATE_LIMITED.
| Module | Endpoints | Scope | Default |
|---|---|---|---|
| Agreements | POST …/newsletter/subscribe/ | agreements_subscribe | 5/min |
| Agreements | POST …/consents/, …/consents/withdraw/ | agreements_consent | 20/min |
| Agreements | …/consents/confirm/, …/consents/unsubscribe/ | agreements_token | 10/min |
| Agreements | POST …/orders/{order_id}/agreements/ | agreements_order | 20/min |
| Agreements | POST …/cookie-consents/ | agreements_cookie_consent | 30/min |
| Checkout | PATCH …/carts/{cart_id}/payment/ | checkout_cart_payment | 30/minute |
| Contact forms | POST …/submit/ | contact_forms_submit | 30/hour |
| Contact forms | GET …/form-types/ | contact_forms_form_types | 120/hour |
| Contact forms | …/bookings/ | contact_forms_booking | 10/hour |
| Atlas | GET …/admin/pim-sku/has-changes/ | atlas_change_log_bulk | 120/min |
| Suppliers | GET …/admin/pim-sku/has-changes/ | suppliers_change_log_bulk | 120/min |
| Enrichment | POST …/admin/proposals/, …/proposals/upload-media/ | enrichment_intake | 100/min |
| Enrichment | POST …/admin/tasks/, …/tasks/import-csv/, …/spawn-rules/{key}/run/ | enrichment_spawn | 120/min |
| Enrichment | POST …/admin/proposals/bulk-accept/, bulk-reject/, bulk-undo/ | enrichment_bulk | 30/min |
| Lookup | POST …/admin/search/, …/admin/check/ | lookup_check; lookup_image when the request carries a picture | 60/min; 30/min |
| Pricefighter | POST …/admin/apply/ | pricefighter_apply | 30/hour |
Storefront limits count per client address (the checkout one per address and cart; the agreements ones apply to
callers without a customer token), admin limits per user. Each contact-forms limit also has a ceiling per
application token (scope <scope>_token, 20 times the default) that holds when one token is replayed from many
addresses.
An installation changes a rate by its scope in the service settings:
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["contact_forms_booking"] = "30/hour". The default holds when the scope is
not configured. The Atlas, Suppliers and Enrichment rates are fixed in the module and take no override.
Failed logins have their own counters on POST /api/token/ and on the customer token route. Only wrong
passwords count, a success is never counted, and a blocked login answers 429 with Retry-After before the password
is checked:
| Counter | Default | Setting |
|---|---|---|
| per username and address | 10 in 900 s | AUTH_TOKEN_MAX_FAILURES_PER_USER_IP, AUTH_TOKEN_FAILURE_WINDOW_S |
| per address | 100 in 900 s | AUTH_TOKEN_MAX_FAILURES_PER_IP, AUTH_TOKEN_FAILURE_WINDOW_S |
| per username, any address | 50 in 3600 s | AUTH_TOKEN_MAX_FAILURES_PER_USER, AUTH_TOKEN_USER_FAILURE_WINDOW_S |
Next: Versioning, what the v2 in these URLs promises.
Sources
Section titled “Sources”django_utils/api/v2_errors.py: the envelope and the code map.django_pim/api/admin/pagination.pyandservices/product_service.py: page sizes and the ordering fields of the PIM product list.django_contentdb_translator/api/views.py:shop_idxas the channel identifier,error_idin a 500.django_agreements/api/public/throttling.pydjango_checkout/api/v2/views/cart_views.pydjango_contact_forms/api/public/throttling.pydjango_atlas/api/admin/throttling.pydjango_suppliers/api/admin/throttling.pydjango_enrichment/api/admin/throttling.pydjango_lookup/api/admin/throttling.pydjango_pricefighter/api/admin/throttling.pydjango_access/services/login_guard.py: the failed-login counters.