The API is one Express application behind one HTTPS endpoint per environment. Every route is POST with a JSON body and a JSON response; there are no verbs to remember, only paths. This page is the part that is the same for every route — the reference pages in this section list the routes themselves, generated from the server's own route documentation.
Hosts#
| Environment | Base | Example |
|---|---|---|
| Production | https://core.inmake.dev/api |
POST https://core.inmake.dev/api/public/store |
| Demo | https://demo-core.inmake.dev/api |
POST https://demo-core.inmake.dev/api/public/store |
Two groups live beside /api instead of under it, on the same host: /system/** (the master-only control plane, https://core.inmake.dev/system/…) and /generative/** (the twin of /api/gen that bills the shared wallet). Each reference page prints the group's real base path.
GET on the base itself (https://core.inmake.dev/api, no token) returns the live spec: version, number, env, the header contract and every route — the same document these pages are generated from. The badges at the top of each group page come from it.
Headers#
| Header | Required | Notes |
|---|---|---|
access-token |
always | A store access token — see Authentication. |
identity-token |
per user | A customer-bound token; alone it inherits its parent's access with the identity's customer, next to an access token only the customer is taken from it. |
store |
preferred | The store id. Falls back to store in the body; the header wins when both are present. |
content-type |
always | application/json. |
Groups#
| Group | Base | Who calls it |
|---|---|---|
| Auth | /api/auth |
Anyone with a token — inspect what it may do. |
| Public | /api/public |
Storefronts and browsers with a public token: read-only. |
| Admin | /api/admin |
Servers with a private token: generic document/collection CRUD, uploads, analytics. |
| Process | /api/process |
Carts, orders, payments, subscriptions. |
| Promo | /api/promo |
Credits ledger and coupons. |
| Secure | /api/secure |
Token and secret management (owner/master). |
| Gen | /api/gen |
Metered generative endpoints, billed to the central wallet; identity-token only. |
| Mailing | /api/mailing |
Send a store e-mail template over the store's SMTP. |
| Generative | /generative |
The /api/gen twin (same wallet, same credential). |
| Hooks (outbound) | — | What the platform sends to you when data changes. |
| Hook (inbound) | /hook |
Stripe webhooks — platform-side, shown for completeness. |
| System | /system |
Master-only store and tenant control plane. |
Access evaluation#
The middleware in front of every group looks up the token, matches the request path against the token's access tree segment by segment (public → store, admin → read, …), and either lets the handler run or answers 403. Stock paths get an extra ownership check. auth/* is always allowed so a token can always inspect itself. What a route needs is therefore not printed per route — it is the route's own path.
Errors#
{
"error": "RequestError",
"message": "Missing required parameter: path",
"body": { "…": "your request body, credentials removed" }
}400 bad parameters · 401 no valid token · 402 empty wallet on a paid route · 403 the access tree said no · 404 not found · 409 conflict · 500 internal (the message is the exception). Details in Authentication › Errors.
Batch paths#
Routes that take a path (admin/read, admin/update, …) also accept a list of paths; the evaluation runs per path and the response is a list in the same order. This is how the SDK's batch calls work.
Listing and pagination#
List routes share one shape: query (a set of conditions — a scalar value is an equality shorthand), order_by ({field, direction}), limit, offset (the id of the last document you received) and mask (return a subset of fields). Page by passing the last id back as offset.
Query keys#
Equality filters alone, or a range / order_by on one field with no filter, need nothing. A listing that filters and sorts — equality on some fields, then a range or an order_by on another — runs on a query key of the collection: built in for the common shapes (below), declared for the rest as keys: {"<name>": ["<field>", …]} on the collection row (the console's collection settings; for a module, the manifest's data.collections[].collection.keys). Rule of thumb: equality on the leading fields, range or sort on the last — {"status_prep_time": ["status", "prep_time"]} serves status == public ordered by prep_time in either direction, with an optional range on it. The platform maintains a computed _k.{name} field on every document (never write it; the public routes never return it) and rewrites a matching query onto it, so no composite index is involved. Limits: up to 8 keys per collection, 1–4 fields each, no id, no _k. A query no key serves still runs while its equality part matches at most 500 documents; above that the answer is 400 {"error": "query_key_required", "collection": …, "suggest": [fields]} — suggest is the field list to declare. With the built-in keys this only happens for a shape outside them.
Built-in keys#
Most listings need nothing declared. Every content collection gets keys derived from its schema template (schema order decides "first"; up to 5, on their own budget — the 8 explicit keys are untouched):
| Built-in key | Fields | Serves |
|---|---|---|
status_timestamp |
[status, timestamp] |
"public X, newest first" — every collection, with or without a template |
status_{d} |
[status, d] for the first two datetime fields d |
"public articles by publish_date", date ranges |
{e}_{d1} |
[e, d1] for the first enum field e; d1 = the first datetime field, else timestamp |
"category X, newest first" |
status_{e}_{d1} |
[status, e, d1] |
the same, public only |
So a recipes collection with an enum category and no date field has status_timestamp, category_timestamp and status_category_timestamp; articles with a publish_date has status_timestamp and status_publish_date. Nothing is derived for number, boolean or array fields — a "by prep_time" listing declares its own key.
The products mirror (data/{locale}/products, what a storefront lists) has a fixed set, and its array filters run on platform composite indexes instead:
| Products | Shape | Serves |
|---|---|---|
status_price |
[status, price.price] |
public products by price, either direction |
status_name |
[status, name] |
public products A–Z |
status_timestamp |
[status, timestamp] |
public products, newest |
| 16 composites | categories / tags array-contains × sort by price.price asc, price.price desc, name asc, timestamp desc — each plain and with status == public |
category and tag pages; array-contains-any (a multi-select) uses the same index |
One array filter per query: "category X and tag Y" is not one index — filter the second array in memory over the category page, or add a scalar field and a key. The mirror carries status and timestamp from the root product, so a product never opened in the content editor is listable too.
A built-in is disabled with keys: {"<name>": null} and overridden by declaring the same name. Built-ins backfill lazily — the first listing after they appear starts the build (building → ready; the query runs natively meanwhile).
→ the full list, generated from the index file: Indexes
Locales#
Content, slugs, templates and translations are per locale in the xx_YY form (cs_CZ). A locale the store does not declare is a 400.
Credits#
The gen/* and generative/* routes are metered; each shows its price in credits next to the path. 1 credit = $0.01 by convention. See Credits.