API changes#
Records behavioural changes that affect integrations.
Current state#
| Area | State |
|---|---|
| Version | v1 |
| Authentication | mandatory application key X-API-Key + optional user JWT with refresh token rotation — see Authentication |
| Permissions | enforced |
| Warehouse module | read-only from outside |
| Specification | covers only the auth endpoints; every operation requires apiKeyAuth |
Known limitations#
Things we know about and that may change:
| Limitation | Note |
|---|---|
| Parameters in the URL | writes included; the body is not read (except sign-in and password actions) |
| Two response shapes | reads without an envelope, writes with one |
| Errors with status 200 | only 400, 401, 403 and 429 are distinguished |
| Inconsistent error envelope | sometimes three keys, sometimes four |
| The HTTP method is ignored | GET on a delete action deletes |
| Owner collections | get-all and get-by are denied to owners |
| Missing HTTP wrappers | the cart, checkout, "my" listings |
| Silent discarding of write data | the only trace is in the server log |
Treat this list as the contract
The behaviour described here is what you can build on. If it changes, it appears here.
Contract changes since 1 October 2026#
| What changed | Impact on a client |
|---|---|
notification: the field userId is now receiverUserId |
filters, sorting and reading responses must use the new name; the old alias does not exist. The value is the notification's receiver, null = shared by all administrators. The database column is unchanged |
A new conditional role receiver |
a new kind of RBAC rule, see Permissions |
response (bazaar, comcat): the action get is available to the receiver |
the advert's owner reads the detail of a reply to their advert; before, administrator only |
response: a new field readKey |
a random secret key for the "Mark as read" link; set by the server, never written from outside. Older replies have it empty |
response: a new internal action mark-read-by-key |
from the site only; no HTTP wrapper |
rating: the action save-reply goes through the role receiver |
the owner of the rated profile replies; before, the role owner handled it |
message-thread, message, message-participant: actions on one conversation are for owner and receiver |
no HTTP wrapper; from outside only the CRUD for an administrator remains |
a new resource external-connection (/api/system/external-connection) |
administrator only; config holds secret keys |
config of external-connection holds secret keys
Only an administrator may read and write it. Do not store the response, do not log it and do not pass it on.
Filtering notifications by userId no longer works
If your client filtered notifications by userId, the filter now fails or is dropped.
Rename it to receiverUserId.
Contract changes of 2 October 2026 (afternoon and evening)#
| What changed | Impact on a client |
|---|---|
response (bazaar, comcat): the currency is required only with a price (price, currencyId) |
a price without a currency ends with the response success: false, code: 2 (validation) and the message "Cena reakce je vyplněná, ale chybí měna." (The reply's price is filled in but the currency is missing.) A currency without a price is dropped (null is stored) and the write succeeds |
response: the price's currency may only be a public monetary currency |
an internal or non-monetary currency (say "Kredity") ends with success: false, code: 2 and the message "Měna #… není veřejná peněžní měna …". Which currencies you may use you learn from currency: active, monetary on, internal off |
a write that contains fields marked FieldAccess … Audience::None is silently trimmed |
the fields password and salt of user, accessToken of message-thread and cancelToken of reservation are not taken from the write body on any path. The response is successful, the field is simply not stored. A user's password changes only through the route the site uses for it |
message-thread: the subject of a conversation from a reply carries the advert's name (bazaar) or the enquiry's subject (comcat) |
the value is raw text without a prefix; "Reakce na inzerát: …" / "Poptávka: …" is put together by the site on display. Older conversations have an empty subject |
a write of a single column (update-columns) with the boolean false |
false is bound as the number 0; before, it failed on a server whose database runs in strict mode |
The codes currencyRequired and currencyNotAllowed do not go on the wire
The server distinguishes two reasons (errorCode = currencyRequired / currencyNotAllowed),
but they exist only in the operation's internal result — the API response carries them only
as the message text and data is null. A client therefore decides by code: 2 and by
what it sent itself, not by the message text (it may change).
Since 19 October 2026 the response carries them in data.errorCode, see
Contract changes since 19 October 2026.
Send the currency only with a price
The simplest client sends currencyId together with price and leaves the currency out
without a price.
Contract changes of 3 October 2026#
Requests and responses#
| What changed | Impact on a client |
|---|---|
a parameter of the wrong shape returns HTTP 400 and code 2 |
text instead of an array (filters, orderBys, columns, data, meta, skipFields, values), missing or empty ids on update-columns, invalid ids on register-event, an unknown boolean spelling (computeTotalCount=yes) and malformed includes are no longer cast and no longer end with a 500. See Error codes |
| an unknown or malformed filter returns HTTP 400 | before, a 500 with HTML. The extra keys inclusive and isNull do not pass in the URL (they expect a boolean) |
get-all: includes=* and columns return HTTP 400 |
before, a 500. A single path as a string (includes=user) now works on a listing too; includes=* stays only on get |
| filters, ordering and columns only over fields you may read | a path to a password, an IP address, another user's personal data or a non-existent field returns HTTP 400 with the message Field path "…" is not allowed in filters, orderBys or columns. See Permissions |
save: text values from the URL are converted by the field type |
data[currencyId]=3 is stored as a number, true/false as a boolean, a date as a date; an empty string on an optional field = null. A value that cannot be converted (1.9 into an integer) returns code 604 with the field name. Before, writing numeric fields from the URL failed. See CRUD pattern |
an owner's save silently drops locked fields |
an owner may edit their account (user/save), but cannot change credits, points, discount, membership, e-mail, username, avatar or the third-party sign-in link — the field is dropped and the database value stays. A comment, rating, advert response or post cannot be moved to another thread or advert. The user's regionId is computed from paymentRegionId. See Editing your own account |
| content creation quota | new responses to an advert or company profile, discussion posts and reports, comments, advert reports and contact messages: 30 per hour per user and 60 per hour per IP. Above the limit success: false, code 404, HTTP 200. See Limits |
Data in responses#
| What changed | Impact on a client |
|---|---|
| other users' personal data: phone, street, postcode, company ID, VAT ID, company name and "about me" | arrive empty for another caller. Read a seller's contact from user/public-profile and user/show-phone |
| IP addresses on adverts, responses, reports, posts, votes, payments and orders | arrive empty for another caller |
| another user's credits, points, discount and discount level | the key is missing from the response; the owner and an administrator see them |
advert and post passwords and keys, the newsletter activation key, a response's readKey, the cart key, the outgoing mail password, token hashes |
nobody gets them over HTTP, not even an administrator. So you will no longer find readKey in a response |
Sign-in and passwords#
| What changed | Impact on a client |
|---|---|
user/update-password requires currentPassword |
without it or with a wrong one it returns code 108; a new password shorter than 8 characters 611. Success returns a new token pair in data and revokes the account's other refresh tokens — store it. See Authentication |
| passwords, keys and tokens only in the POST body | update-password (password, currentPassword), reset-password (email, key, password) and is-auth-valid (token, or the Authorization header) read the body — form or JSON. A value in the URL is ignored |
user/send-new-password sends a link and does not change the password |
the link is valid for an hour; POST user/reset-password sets the new password. An invalid key = code 112 |
| a unified sign-in error | an unknown account and a wrong password both return code 110. An inactive (109) or blocked (104) account is reported only after a correct password. A new limit: 20 failed sign-ins per hour per account from all addresses |
| an access token stops working after blocking, deactivation or a password change | before, it stayed valid until expiry (15 minutes). The client gets a 401 at once |
| account deletion | an account pending deletion cannot sign in (code 117, data.scheduledAt = the deletion date), refreshing its token returns 102; a deleted account cannot be saved (code 120). The request and the cancellation happen only on the website |
| sign-in via a third-party service confirms linking by e-mail | runs on the website; new codes 113–116 |
Removed and internal actions#
| What changed | Impact on a client |
|---|---|
removed actions payment-item/update-status-for-payment (bazaar, comcat, discussion) and user-payment/update-status-for-payment |
they return 404. A payment's status is changed only by the payment gateway and the administration |
payment/express-offers is internal only |
the website composes the express SMS payment offer; there is no HTTP wrapper |
payment/express-execute receives SMS messages from the aggregator |
it answers in plain text per the aggregator's specification; not meant for integrations |
Contract changes of 5 October 2026#
| What changed | Impact on the client |
|---|---|
| sign-in: above the account limit the password is checked only from a known device or address | an external auth/login is always an unknown device (it sends no cookie); only an address with a successful sign-in of the account in the last 30 days helps it. The reply to a throttled request is unchanged (429, Retry-After). See Limits |
| the email after posting an advert no longer carries the password, it carries a management link | the link is valid for 7 days, can be used repeatedly and stops working when the advert password is changed. The password in the outgoing mail queue is masked. Whoever proves access with the advert password sees unmasked personal data of that advert only |
the path key in uploaded files is dropped in external calls |
an external client sends files as base64 in data; a reference to a file on the server is accepted only by internal calls from the website's forms |
get-all without limit returns 50 records, limit is capped at 500 |
a client that relied on a complete listing in one call must page (offset, limit, computeTotalCount=1). A larger limit is not rejected, only cut. Applies to calls from outside |
the response envelope has no exception field |
the keys are success, message, data, code. The exception field used to be always null; a client should not read it |
on a technical failure message is empty, the code decides |
database, file and internal API errors no longer return the exception text. Validation and business-rule failures carry their message as before. See Response format |
Contract changes of 6 October 2026#
The main change: every external API call must carry an application key.
| What changed | Impact on the client |
|---|---|
mandatory application key in the X-API-Key header (HTTPS only) |
the key is issued by the website administrator in Settings → Applications with Api access and shown only once; it identifies the application, not a user. Form: coolcms_ followed by 64 hexadecimal characters. Sign-in, token refresh and sign-out require it too. See Authentication |
| the key and the user token apply together | without Authorization: Bearer the call is evaluated as guest (what an anonymous website visitor sees); with a token, with that user's roles. An invalid or expired token still returns 401 with code 103, it does not silently fall back to guest |
| a missing, wrong, disabled or revoked key | always the same response: HTTP 401, code 501 (API_UNAUTHORIZED), message A valid application key (X-API-Key) is required.. It does not say which case occurred. See Error codes |
| limits per application | defaults 20 per second and 600 per minute per key, shared by all users of the application; under them a per-caller limit (the account from the token, the IP address without a token and on public actions), default 120 per minute. Windows are fixed. The website administrator can change the values per application. See Limits |
| an exceeded limit | HTTP 429, a Retry-After header, a body with the message Too many requests., data.retryAfter and code 404 (RATE_LIMIT_EXCEEDED, not an HTTP status). The special endpoint limits still apply; the sign-in and token refresh limits still return the message Too many requests, retry in <N>s with data.retryAfter |
| the key has no scopes | the key does not decide which data the application gets, nor does it replace signing in. What a call may do is decided by the user's roles (guest without a token) |
| roles allowed per application | the website administrator decides which roles the application may use. Default for a new application: guest, registrated, owner, receiver; administrator roles only explicitly. An administrator through an application without the admin role acts as a regular user (403 on admin-only actions, no admin-only fields, no unmasked personal data). Without owner, even your own records cannot be edited; guest is always allowed (without it nothing could be called without a token and nobody could sign in). See Permissions |
| endpoints that need no key | /api/system/payment/execute, /api/system/payment/delivery, /api/system/payment/express-execute and /api/system/auth/oauth-deauthorize-callback are called by third parties that cannot send a key. Partner applications do not call them |
X-API-Key in CORS |
is an allowed preflight header; a key in an app or browser script is not secret |
| OpenAPI specification | /api-docs.json requires apiKeyAuth on every operation together with optional bearerAuth |
A client without a key gets 401
Add the X-API-Key header to every call. The earlier description of the key as a
replacement for signing in on server calls no longer applies: the key is part of every
request and the user token is added to it.
Contract changes of 6 October 2026 (keys with an expiry)#
| What changed | Impact on the client |
|---|---|
| an application can have several keys (at most 5 valid) | the administrator issues another key without the current one stopping — a key replacement without an outage. See Authentication |
| a key can have an expiry | after it the key gets the same response as an unknown key: HTTP 401, code 501. The expiry can also be set later (rotation: expiry on the old key, a new key, deployment, deletion after the expiry). Read the key from configuration so that replacing it needs no new code |
| revoking a single key | a revoked key returns 401 with code 501 at once, the other keys of the application keep working |
| limits belong to the application | the defaults of 20 per second and 600 per minute are shared by all keys of the application (previously “per key”, the application had one) |
Contract changes since 19 October 2026#
Main change: the API checks writes by a non-privileged caller the same way as the website
forms, and external advert reads return only what the website shows. The administrator
(admin role) is neither checked nor filtered.
Reads#
| What changed | Impact on the client |
|---|---|
advert (bazaar, comcat): get, get-by, get-all and search return only adverts and profiles that are active and not expired (expirationDate today or later), or own ones (insertUserId = the user from the token) |
a hidden record disappears together with its relations (parameters, categories, images, files, texts); get of a hidden one looks like not found. A page may hold fewer records than limit, totalCount is an upper bound. An expired advert used to stay in listings. See Bazaar |
category (bazaar, comcat): only active categories |
a disabled category is not returned |
blog/carousel: get-all is public |
readable by a guest (application key only) and by a signed-in user |
Writes — new refusals#
| What changed | Impact on the client |
|---|---|
a refused save carries data.errorCode |
a stable name of the reason; data used to be null on failure. Other details (parameter id, missing fields, type of the blocked value) are only in the message text. See Error codes |
advert/save (bazaar, comcat): parameter rules as in the form |
code: 2 with errorCode parameterTooLong, parameterUrlNotAllowed, parameterBlacklistedWord (active forbidden words in the request page language), parameterRequired (required by the parameter rules, e.g. price type) and parameterOptionNotAllowed. Required parameters are checked on insert; uploads and hidden fields are not |
advert/save: a disabled or administrator-only category |
code: 615, errorCode: advertCategoryNotAllowed |
advert/save: a new image over the size limit |
the image is not saved, the rest of the write passes; saveInfo.rejectedUploads contains {"name": …, "reason": "size"}. Limit: the parameter's max_size, else the maxImageSize setting, else 50 MB |
advert/extend-expiration in a paid category |
code: 607, data: null — the extension is paid on the website |
user/save: the account type requires a company ID and company name or a VAT ID |
code: 2, errorCode: userTypeDataMissing; only when the write carries the account type. See System |
| blacklist of e-mails, phones and IP addresses (CIDR ranges too) | new comments, newsletter subscribers, adverts and profiles, replies, saved searches, contact messages, discussion posts, complaints, orders, reservations and new accounts with a blocked value: code: 615, errorCode: blacklisted. For an account edit, only a changed e-mail or phone. See System |
comment/save into a thread with comments switched off |
code: 607, errorCode: commentsClosed |
a guest comment/save where only signed-in users may comment |
code: 400, errorCode: commentOnlyForLoggedIn (HTTP 200) |
response/save (bazaar) to a sold advert |
code: 607, errorCode: advertSold |
forbidden words are one list (system/blacklisted-word) |
adverts, the company directory and the discussion share the system list; there are no per-module lists |
Contract changes since 29 October 2026#
| What changed | Impact on the client |
|---|---|
a technical failure (database, file, external service, queue, unknown error — e.g. code 1, 300, 500) no longer carries the exception text in message |
message is the generic "An internal error occurred. Quote the reference when contacting support." and the response gets an extra key errorRef (a short random code). The other keys (success, code, errorCode, data) do not change; domain errors (validation, state, not found) stay as they were, without errorRef. See Error codes |
Behaviour on change#
The permission switch applies to the whole API
Changing the mode changes behaviour across every resource at once. Your client must handle a denial on an action that used to work.
Write your client defensively
Check the content type, the presence of keys and the status code. Responses may differ in detail from resource to resource.
What is coming#
| Item | State |
|---|---|
| Extending the specification to CRUD resources | planned |
| HTTP wrappers for missing operations | as integrations need them |
Need something that is not available?
Contact the operator. Many operations exist and merely lack an HTTP wrapper — adding one is a matter of one resource, not a new feature.