HTTP API

The delivery API under every SDK — reads, the release pointer, forms, live data, site users and the publish webhook.

Every SDK is a thin layer over a plain HTTP API, so any site — in any language — or a build script can read Mapled. The base URL is https://api.mapled.io; every request carries the delivery key in x-mapled-key.

Endpoints

RequestAnswer
GET /v1/delivery/collections/{collection}/records{ records: [{ id, data, expanded? }], total, release }
GET /v1/delivery/collections/{collection}/records/{id}{ record, release } — 404 when it isn't in the release
GET /v1/delivery/collections/{collection}/records/by-slug/{slug}{ record, release }
GET /v1/delivery/singles/{single}{ record, release } — record is null while the single is empty
GET /v1/delivery/release{ release, publishedAt } — the release being served now; nulls before the first publish. With ?webhook=1 also webhook: { release, publishedAt, deliveredAt, cache } of the newest release your site answered the publish webhook for, with the x-mapled-cache mark the answer carried (nulls before the first, and again after the webhook's address changes), or null when the project has no webhook
POST /v1/delivery/forms/{form}201 { ok: true } — see Forms
curl -H "x-mapled-key: $MAPLED_KEY" \
  "https://api.mapled.io/v1/delivery/collections/articles/records?filter[featured]=true&filter[date][gte]=2026-01-01&sort=-date&limit=10&expand=author&fields=title,slug"

Queries

Lists take filter[field]=value or filter[field][op]=value (eq, ne, lt, lte, gt, gte, in — comma-separated, contains), sort=field / sort=-field, limit (1–100, default 100) and offset. search=text is full-text search of a list: every word has to be in the record, the last one may be the start of a word, the best match comes first unless you pass sort (a match in the title weighs more), text and choice fields are searched and a sensitive field never; the query is 1–200 characters and the answer names it as search.query. Every read takes expand (relation keys, dotted for a second level), fields, release=N to read a published release by its number and locale=ru to read in one of the project's languages: a translated field answers in it or in the first language of the project's fallback chain that has a value, filters and slug lookups see that language, and the answer carries locale. A query the schema can't answer is a 400 INVALID_QUERY that says why.

What a key reads

Only published content of collections with access Public is served; sensitive fields are never in an answer. Image and file values are asset ids — https://api.mapled.io/files/{id} is the file, with ?w=, h=, fit=, fmt=, q= for a resized variant.

Caching

Answers are per key — Cache-Control: private — with an ETag: send it back as If-None-Match and an unchanged answer is a 304. A read pinned with release=N never changes and may be cached for good. GET /v1/delivery/release is the cheap way to learn that something was published: relay it from a route of your own server, and watchRelease({ endpoint, onPublish }) from @mapled/next — no framework in it — tells open tabs (Live updates).

Errors and limits

Errors share one envelope: { error: { code, message, requestId } }. A key gets 1,200 requests a minute; past that, 429 RATE_LIMITED with retry-after.

The publish webhook

The publish webhook is a POST with a JSON body { event, project, release, at, id } and the headers x-mapled-event (release.published, project.restored or test), x-mapled-event-id (the same on every retry — use it to deduplicate), x-mapled-attempt and x-mapled-signature: sha256= followed by the hex HMAC-SHA256 of the raw body with the signing secret. Compare in constant time, answer 2xx within 5 seconds. verifySignature from @mapled/next/server does the check anywhere Web Crypto exists. An answer may carry x-mapled-cache — up to 64 letters, digits, - or _ naming the cache the handler revalidated; Mapled records it with the release, and the ?webhook=1 pointer gives it back.

Live data

Live data: GET / POST /v1/app/collections/{collection}/records and GET / PATCH / DELETE /v1/app/collections/{collection}/records/{id} with the server key in x-mapled-server-key; writes send { "data": { … } }. On a collection for signed-in users add the person's session token in x-mapled-site-session to act as them (the record carries owner); the key alone is the backend. A session that ended is a 401 SESSION_INVALID.

Site users

Site users, with the server key too, JSON bodies, tokens never in URLs: POST /v1/app/identity/sign-up { email, password, profile? } → 201 { user }, /v1/app/identity/verify { token, password } → { user, session: { token, expiresAt } } (the sign-up password; a wrong one is the 400 TOKEN_INVALID of a bad token and spends nothing, none at all a 400 PASSWORD_REQUIRED), /v1/app/identity/sign-in { email, password } → { user, session: { token, expiresAt } }, /v1/app/identity/session { token } → { user, session: { expiresAt } } or 401 SESSION_INVALID (reason: "suspended" when so), /v1/app/identity/sign-out { token } → 204, /v1/app/identity/reset/request { email } → 202, /v1/app/identity/reset { token, password } → 204. Off until a member turns Site Identity on (404 IDENTITY_DISABLED).