CRUD pattern#
All resources share the same set of actions. Every call below (addresses are shortened to the
path) carries the X-API-Key header with the application key;
writes and "my" data additionally carry Authorization: Bearer <token>.
🔴 Parameters go in the URL#
The request body is not read at all for CRUD actions
Neither form-encoded nor JSON. Everything — including the data to be saved —
goes in the query string in PHP array notation:
data[title]=…&data[category][id]=3.
The exceptions are the authentication endpoints, which conversely
read a form body, and the actions with passwords and keys (update-password,
reset-password, is-auth-valid), which read them only from the body — form or
JSON.
The HTTP method does not matter#
GET /api/…/delete?id=5 really does delete
The action comes from the path, not the method. Do not put API addresses into prefetching or anywhere a crawler will follow them.
Parameter shapes are checked#
A parameter of the wrong shape is not cast "somehow" — the request ends with HTTP 400 and
code 2 before anything is read or written. Text instead of an array (filters=x), an
unknown boolean spelling (computeTotalCount=yes) or a list instead of a single ID therefore
does not return different data but an error. The full overview is in
Error codes.
Actions#
| Action | Purpose |
|---|---|
get |
one record by id |
get-by |
one record by filter |
get-all |
a listing |
save |
insert and update alike |
update-columns |
changing selected fields |
delete |
deletion |
delete-all |
deletion by filter |
Read parameters#
| Parameter | Meaning |
|---|---|
id |
identifier |
filters |
filters, see below |
orderBys |
ordering |
offset, limit |
pagination; see the note on the limit below |
includes |
loading relations |
computeTotalCount |
1 computes the total; only 1/0, true/false, on/off |
columns does not work on a listing from outside
Narrowing the returned fields with columns on get-all ends with HTTP 400. The
response carries every field you may read.
A listing without limit returns 50 records, at most 500
For an external get-all, a missing, non-numeric or smaller-than-1 limit returns 50
records. A larger value is cut to 500 — it is not an error, the response simply
holds at most 500 records. Fetch further records by paging: repeat the call with a growing
offset and stop by totalCount (send computeTotalCount=1).
Ordering#
The direction is a truthy value, not asc / desc
orderBys[name]=desc is a non-empty string, therefore true — it sorts
ascending. Use 1 and 0.
Relations#
includes is one path or a list of paths.
includes=* works only on get
On a detail it pulls in every relation — an easy route to a huge response. On
get-all it ends with HTTP 400. List what you need.
Filters#
A filter is a triple [type, field, value]:
?filters[0][0]=equal&filters[0][1]=activated&filters[0][2]=1
&filters[1][0]=like&filters[1][1]=title&filters[1][2]=notebook&filters[1][matchType]=both
&filters[2][0]=in&filters[2][1]=id&filters[2][2][]=1&filters[2][2][]=2
Filters combine with AND.
| Type | Extra keys |
|---|---|
equal, notEqual |
|
like, notLike |
matchType: both, start, end |
in, notIn |
|
isNull, isNotNull |
|
between |
from, to |
range |
inclusive |
greaterThan, greaterOrEqual |
short forms gt, gte |
lessThan, lessOrEqual |
short forms lt, lte |
comparison |
operator |
date, dateBetween, dateAfter, dateBefore |
operator, format |
group, or, and |
filters, logic |
fulltext |
mode, minScore |
An unknown or malformed filter returns 400
A typo in the type name, a filter without a field or a malformed nested group ends
with HTTP 400 and code 2. The extra keys inclusive and isNull expect a boolean,
which a URL cannot carry — do not use them in the query string.
You may filter and order only by fields you may read
A filter or ordering over a password, an IP address, another user's e-mail or a
non-existent field returns HTTP 400 with the message Field path "…" is not allowed.
See Permissions.
Saving#
One endpoint for insert and update. They are distinguished by the presence of
data[id].
data, meta and skipFields must be arrays; text instead returns HTTP 400. Empty
data returns code 602.
Values are converted by the field type#
From the URL every value arrives as text. The server converts it by the type of the field it belongs to:
| Field type | Accepts | Rejects |
|---|---|---|
| integer | 12, -3, 0 |
1.9, 1e2, +1, 01, whitespace |
| decimal | 1.5, -0.25, .5, 2e3 |
a comma, whitespace |
| boolean | 1, 0, true, false, on, off (lower case) |
yes, ano, TRUE |
| date and time | whatever PHP new DateTime() reads, e.g. 2026-10-03 14:30 |
a non-existent date (2026-02-31), surrounding whitespace |
| text | anything | — |
An empty string means null for a field that may be empty; for a required one it is an
error. A value that cannot be converted returns code 604 with the field name and
nothing is saved:
{"success":false,"code":604,"message":"Invalid value for field \"currencyId\": expected int, got \"1.9\"."}
Part of your data is silently discarded#
The response reports success but the value is not saved
Discarded are:
meta— entirely, for most resources,- fields marked read-only from outside (the server sets the value),
- fields your account may not write — an owner cannot change, for example, the credits, discount or e-mail of their account (see Editing your own account),
- nested collections the resource does not permit.
On update the original value stays in the database, on insert the default. The only trace is in the server log. After writing, read the data back and check.
The server fills in the owner and the IP address
On insert the owner comes from the sign-in and the IP address from the request. Any user identifier or address you send is not used.
Editing your own account#
A signed-in user may edit their own account through /api/system/user/save (data[id]
= their id; another id ends with 403).
| May change | Will not change (the field is silently dropped) |
|---|---|
| first and last name, phone, street, city, postcode, state and country | username and e-mail |
| company, company ID, VAT ID, the company flag, user type | credits, points, discount and discount level |
| payment region, language, website, "about me" | membership (memberUntil) |
avatar (assetPath), third-party sign-in link (type, externalId) |
|
the password — only update-password changes it |
The region is derived from the payment region
The server computes the user's regionId from paymentRegionId; a value you send is
overwritten.
A deleted account cannot be saved
For an anonymized account the save returns code 120.
Similar fields are locked on the other records an owner edits: you cannot move a comment, a
rating or an advert response to another thread or advert, and you cannot set moderation or
"read" flags through save.
Content creation quota#
New responses to an advert or a company profile, discussion posts and reports, comments,
advert reports and contact form messages have an hourly cap. Above it save returns
success: false and code 404 (with HTTP 200). The values are in
Limits.
Changing selected fields#
ids must be a non-empty array and values an array; otherwise HTTP 400.
From outside this does not work on most resources
The list of permitted columns defaults to empty, so the call ends with code
607. It works only for columns with an explicit exception.
No partial writes
One non-permitted column brings the whole call down.
Deleting#
Bulk deletion never passes the ownership check
For the owner role it is always denied.
delete-all without filters deletes the whole collection
If the caller may use the action, a missing filter means "everything". A malformed filter, on the other hand, returns HTTP 400 and deletes nothing.
Tree resources#
Categories and menus additionally offer move-up, move-down, move-subtree,
move-subtree-to, insert-tree-node and delete-node.
delete-node deletes the whole subtree
Not just the node.
Events#
register-event increments a counter by one:
Permitted counter names are restricted by a list. ids must be a non-empty list of positive
integers, otherwise HTTP 400.
🔴 Many operations are unreachable from outside#
A resource may exist and still return 404
The system contains many operations used only internally by the website that have no HTTP wrapper. These include the cart, order checkout, "my" listings, availability and current prices.
You recognise it because the action returns 404 with HTML even though the resource and the permission both exist.