Querying
List endpoints (GET /api/:type) take query parameters to filter, sort, page and shape the response. They combine freely, except where noted. The same filters work in GraphQL and the JavaScript client.
curl -g "http://localhost:3000/api/blog?where[category][eq]=news&sort=createdAt:desc&limit=10"Parameters
Section titled “Parameters”| Parameter | Example | Does |
|---|---|---|
where |
where[price][gte]=10 |
Filter on fields (operators) |
filters |
filters[price][$gte]=10 |
Same as where |
sort |
sort=slug:asc |
Order of results (sorting) |
page, limit |
page=2&limit=50 |
Page number and size (paging) |
cursor |
cursor=eyJzIjoi… |
Keyset paging from meta.nextCursor |
search |
search=headless cms |
Full-text search (search) |
fields |
fields=title,cover |
Only return these data fields |
populate |
populate=author |
Inline related entries |
locale, fallback |
locale=fr&fallback=true |
Pick a language (languages) |
stage |
stage=draft |
Include unpublished entries (needs credentials, see Stages) |
Filtering with where
Section titled “Filtering with where”The form is where[field][operator]=value. field is a field key from the content type, or one of the entry’s own properties:
# data fieldsGET /api/product?where[price][lt]=50GET /api/product?where[featured][eq]=true
# the entry's own properties: slug, locale, createdAt, updatedAt, publicIdGET /api/blog?where[slug][startsWith]=2026-GET /api/blog?where[createdAt][gte]=2026-01-01T00:00:00.000ZWithout an operator, where[field]=value means equals:
GET /api/blog?where[category]=newsSeveral conditions are combined with AND.
Operators
Section titled “Operators”| Operator | Meaning | Example |
|---|---|---|
eq |
equals | where[status_label][eq]=open |
ne |
not equal | where[category][ne]=archive |
gt, gte |
greater than (or equal) | where[price][gte]=10 |
lt, lte |
less than (or equal) | where[price][lt]=100 |
in |
any of a comma-separated list | where[category][in]=news,guides |
notIn |
none of the list | where[category][notIn]=draft,internal |
contains |
text contains | where[title][contains]=Node |
startsWith |
text starts with | where[title][startsWith]=How |
endsWith |
text ends with | where[title][endsWith]=guide |
Operators may be written with a $ prefix ($eq, $gte, $in), Strapi style, and filters[...] is accepted as an alias of where[...].
How values are read:
trueandfalsebecome booleans.- Numbers such as
10or4.5become numbers. A value with leading zeros, like007, stays text. - Everything else is text.
Good to know:
contains,startsWithandendsWithare case-sensitive on data fields and case-insensitive onslugandlocale.wherefilters top-level fields. Values inside a group, repeater or flexible field can’t be filtered, except through a relation (below).- An unknown operator is treated as
eq. - To filter a field often on a large content type, mark it indexed. See Indexed fields.
OR and AND
Section titled “OR and AND”Group conditions with OR and AND, each clause numbered:
# category is news OR the post is featuredGET /api/blog?where[OR][0][category][eq]=news&where[OR][1][featured][eq]=true
# price between 10 and 50, and (in stock OR preorder)GET /api/product?where[price][gte]=10&where[price][lte]=50&where[OR][0][stock][gt]=0&where[OR][1][preorder][eq]=trueA clause can hold several conditions; they are combined with AND inside the clause.
Filtering by related entries
Section titled “Filtering by related entries”On a relation field, where can filter on the related entry’s fields, one level deep:
GET /api/post?where[author][slug][eq]=jane-doeGET /api/post?where[author.name][eq]=Jane%20DoeThis matches at most 1,000 related entries. See Filtering by related fields.
Legacy filter
Section titled “Legacy filter”filter[field]=value is an older form that matches entries whose field contains the value as text:
GET /api/blog?filter[title]=helloPrefer where, which has operators and exact matching.
Sorting
Section titled “Sorting”sort=field:direction:
sort |
Order |
|---|---|
createdAt:desc |
newest first (default) |
createdAt:asc |
oldest first |
updatedAt:desc / updatedAt:asc |
by last change |
slug:asc / slug:desc |
alphabetical by slug |
Only createdAt, updatedAt and slug can be sorted on. Any other field falls back to createdAt, and any direction other than asc means desc.
Paging
Section titled “Paging”By page
Section titled “By page”GET /api/blog?page=2&limit=50limit is 1 to 100 (default 20). Values outside that range are clamped. meta tells you where you are:
{ "total": 132, "page": 2, "limit": 50, "totalPages": 3, "nextCursor": "eyJzIjoiY3JlYXRlZEF0OmRlc2MiLC…" }By cursor
Section titled “By cursor”Deep pages get slower with page, because the database counts and skips every earlier row. A cursor continues from the last entry of the previous page and stays fast at any depth:
GET /api/blog?limit=100# → meta.nextCursor = "eyJzIjoi…"GET /api/blog?limit=100&cursor=eyJzIjoi…Every list response includes meta.nextCursor, null on the last page. In cursor mode meta is only { limit, nextCursor }: there is no total, which is what makes it fast.
- Keep the same
sortfor every page. A cursor used with a different sort returns400. - Keep the same filters too; the cursor only remembers the position.
- A cursor can’t be combined with
search. Page search results withpageinstead.
Search
Section titled “Search”search runs a full-text search over the slug and every field value:
GET /api/blog?search=headless cms{ "data": [ … ], "meta": { "total": 4, "page": 1, "limit": 20, "totalPages": 1, "searchMode": "fulltext" }}meta.searchMode tells you how the search ran:
searchMode |
Meaning |
|---|---|
fulltext |
PostgreSQL full-text index (English word forms: “posts” matches “post”) |
fallback |
A plain text match, used when the term can’t be read as a full-text query |
meilisearch |
An external Meilisearch engine is configured. See Search |
The full-text query accepts web-search syntax: "exact phrase", -excluded, this OR that.
Choosing fields
Section titled “Choosing fields”fields limits data to the listed keys. The entry’s own properties (id, slug, locale, seo, dates) are always returned:
GET /api/blog?fields=title,cover{ "id": "8c0f…", "slug": "hello-world", "locale": "en", "data": { "title": "Hello world", "cover": { "url": "…", "alt": "" } }, "…": "…" }fields works on single entries too: GET /api/blog/hello-world?fields=title.
Related entries
Section titled “Related entries”populate replaces relation IDs with the related entries, with dot paths for deeper levels:
GET /api/post?populate=author,categoriesGET /api/post/my-post?populate=author,author.companySee Relations for depth limits and what’s included.
Responses with fields or populate aren’t cached, so use them where they save real work.
Languages
Section titled “Languages”On a site with several content languages:
| Request | Returns |
|---|---|
GET /api/blog |
entries in every language |
GET /api/blog?locale=fr |
French entries only |
GET /api/blog?locale=fr&fallback=true |
French entries, plus default-language entries that have no French translation |
GET /api/blog/hello |
the default-language entry |
GET /api/blog/hello?locale=fr |
the French translation, or 404 |
GET /api/blog/hello?locale=fr&fallback=true |
the French translation, or the default-language one when there isn’t any |
Each entry’s locale says which language you got. A single-entry response also sets the Content-Language header to it.
Single entries
Section titled “Single entries”GET /api/:type/:slug takes locale, fallback, stage, fields and populate. Filters, sorting and paging only apply to lists.