Strapi delivery
Strapi’s own paths, unprefixed. A site written against Strapi v5 needs its base URL changed and nothing else.
GET /api/:plural a page of entriesGET /api/:plural/:documentId one documentPOST /api/:plural createPUT /api/:plural/:documentId updateDELETE /api/:plural/:documentId deleteResolving the collection
Section titled “Resolving the collection”Strapi has no concept of a space, because one Strapi project is one space. A plural in the path is therefore looked up across every space the tenant declares.
- One match is the answer.
- No match is
404, and the message lists the spaces. - Several matches is
400, and the message lists the qualified names to choose from. Qualify the path segment asblog.postto select one.
Reading
Section titled “Reading”Locale and publish state
Section titled “Locale and publish state”| Parameter | Default | Meaning |
|---|---|---|
locale | every locale | Select one locale’s rows. |
status | published | draft shows unpublished entries. Anything else is 400. |
status=draft shows the draft view and does not mix published entries into it, which is what an
editor checking unfinished work wants.
Drafts require permission to edit the type, or a preview token.
Filters
Section titled “Filters”Strapi’s bracket syntax, on the entity’s own fields and through declared relations.
?filters[title][$eq]=Hello world?filters[price][$gt]=200?filters[slug][$in]=hello,second?filters[author][name][$eq]=Ada # through a relation| Operator | Meaning |
|---|---|
$eq | equal. The default when no operator is given |
$ne | not equal |
$lt $lte $gt $gte | ordered comparison |
$in $notIn | membership. Comma-separated, or repeated keys |
$contains $notContains | substring |
$startsWith $endsWith | prefix and suffix |
$null $notNull | presence. Takes a flag, not a value |
Two filters are a conjunction. Alternatives use an indexed $or group, where the index identifies
the branch:
?filters[$or][0][slug][$eq]=hello&filters[$or][1][slug][$eq]=secondTwo keys sharing one index are a conjunction within that branch.
An operator outside the table is refused with 400 rather than ignored. A dropped predicate returns
more rows than were asked for, which is the direction that exposes content.
A filter naming a field the type does not declare is 400. A value the column cannot hold is 400
and names the problem, rather than reaching the backend and returning an internal failure.
Sorting
Section titled “Sorting”?sort=title:asc?sort=title:desc,createdAt:ascRepeated sort=, the sort[]= array form and indexed sort[0]= are all accepted, which covers
both hand-written URLs and what a client library emits. A direction other than asc or desc is
400, as is an unknown field.
Selecting fields
Section titled “Selecting fields”?fields=title,slugid, documentId and locale are always projected, whether or not they were listed. A response
whose documentId depended on the request would break every client that follows a link.
Populating relations
Section titled “Populating relations”?populate=author?populate=author.publisher # two levels, named?populate[0]=author # indexed form?populate[author][populate][publisher] # object form, same as author.publisher?populate[author]=false # switched offRelations are resolved one query per relation per level, batched across the level. The cost is
depth × distinct relations and does not move with the number of rows returned. Twenty-five posts
sharing three authors and two publishers is three queries.
The depth ceiling is 10. A deeper request is refused with the number.
Resolving a relation goes through the target type’s own read rule, so a caller sees only what
they are entitled to.
A populated relation is rendered inline, as a nested entry.
Pagination
Section titled “Pagination”| Parameter | Default | Notes |
|---|---|---|
pagination[page] | 1 | |
pagination[pageSize] | 25 | Clamped to 100 |
pagination[start] | 0 | The offset form |
pagination[limit] | 25 | Clamped to 100 |
pagination[withCount] | true | false omits pageCount and total, and saves a query |
A page with no explicit sort is ordered by (documentId, locale), which is arbitrary but stable, so
page two never repeats a row from page one.
Field name aliases
Section titled “Field name aliases”The surface speaks Strapi’s vocabulary in both directions.
| Strapi | Stored as |
|---|---|
documentId | documentid |
createdAt | createdon |
updatedAt | updatedon |
publishedAt | publishedon |
Response shapes
Section titled “Response shapes”Entries are flat, as in Strapi v5. The fields sit directly on the data object.
A page:
{ "data": [ { "id": "01J9Z…", "documentId": "01J9Y…", "locale": "en", "title": "Hello world", "createdAt": "2026-09-15T09:12:44Z", "updatedAt": "2026-09-15T09:14:02Z", "publishedAt": "2026-09-15T09:14:02Z" } ], "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 1 } }}One document:
{ "data": { "id": "01J9Z…", "documentId": "01J9Y…", "title": "Hello world" }, "meta": {} }A document that does not exist in the requested locale is 404 with the error envelope, rather
than a null body, so a client can tell it apart from an empty field.
An error:
{ "data": null, "error": { "status": 400, "name": "ValidationError", "message": "unknown field \"nope\" on blog.post" } }| Status | name | When |
|---|---|---|
400 | ValidationError | The query was malformed or named something unknown |
403 | ForbiddenError | Well formed, and the caller is not entitled |
404 | NotFoundError | No such type, or no such document in that locale |
400 | ApplicationError | The request could not be served |
Bookkeeping columns are never rendered: createdby, updatedby, deletedby, deletedon and
status. status selects which entries are visible and is not itself content, so echoing it would
invite a client to filter on the response instead of the query. This holds at every level, so a
populated relation does not expose them either.
Writing
Section titled “Writing”The body is {"data": {...}} and the reply is {"data": {...}, "meta": {}}, so a client’s own
serialisation round-trips.
Authorization is the entity’s own access rules, carried through the same hook chain as any other write to the entity.
Create
Section titled “Create”POST /api/posts?locale=enContent-Type: application/json
{"data": {"title": "Hello world", "slug": "hello"}, "publish": false}201 Created, with the row that landed.
A new entry is a draft. Publishing whatever was posted would mean one forgotten parameter puts an unfinished entry on the live site.
Update
Section titled “Update”PUT /api/posts/01J9Y…?locale=enContent-Type: application/json
{"data": {"title": "Hello, world"}, "publish": true}200 OK. publish moves the publish state; omitting it leaves the state alone.
Delete
Section titled “Delete”DELETE /api/posts/01J9Y…?locale=en204 No Content. The row is gone, so there is nothing truthful to return about it.
Rules that apply to every write
Section titled “Rules that apply to every write”The locale is part of the address. A document is one row per locale, so a write without a
locale parameter writes the default locale rather than every translation.
Identity columns are stripped, not refused. A caller may not choose its own documentId, since
it could collide with or impersonate another document. These arrive naturally when a client
round-trips an entry it has just read, so they are removed in both spellings rather than rejected.
Fields marked localized: false are propagated across the document’s locale rows.
The body is bounded at 8 MiB. An entry is text somebody wrote; bytes belong in the asset plane,
behind a field of type file. A body over the cap is 400 and says so, rather than failing as
malformed JSON.
Only content types are addressable. A caller names a content type and reaches it through the same resolution the read surfaces use, so the schema’s other entities are not reachable here.
Write errors
Section titled “Write errors”| Status | name | When |
|---|---|---|
400 | ValidationError | The body was not {"data": {...}}, was too large, or a value was rejected |
403 | ForbiddenError | The entity’s rules refused this caller |
404 | NotFoundError | No such type, or no such document in that locale |
See also
Section titled “See also”- Contentful delivery: the same content, CDA-shaped
- Preview: rendering drafts from a front end
- The content model: where relations and locales are declared