# Query parameters

Every collection endpoint (`/api/{entity_type}/{bundle}`, see the [Content API reference](/source-cms/reference/content-api/#endpoint-shape)) accepts the [JSON:API](/start-here/glossary/#jsonapi) query parameters below. Parameters combine freely with `&`. Field paths always use [field](/start-here/glossary/#field) [machine names](/start-here/glossary/#machine-name), exactly as they appear as keys in responses, not admin-UI labels. A self-managed Drupal site on Cloud Platform serves its own JSON:API rather than the Content API; for its parameters, see [Drupal's JSON:API documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module).

<TryItAside />

For learning-oriented walkthroughs, see the [query quickstart](/source-cms/content-api/quickstart/) and the [querying guide](/source-cms/content-api/guide/).

> All fifteen operators below work on Source CMS. Two caveats: **date fields compare as UNIX timestamps** (see the operators section), and filtering the computed `moderation_state` field returns a server error (503) with **any** operator, including plain equality; filter on `status` instead.

## `filter`: match entries by field value

### Short form (equality)

```
?filter[field_name]=value
```

Example: published entries only (`fields` trims the response for display, see [sparse fieldsets](#fieldstype-sparse-fieldsets)):

```
GET /api/node/article?filter[status]=1&fields[node--article]=title
```

```jsonc
{
  "jsonapi": { "version": "1.1", "meta": { … } },
  "data": [
    { "type": "node--article", "id": "7f7dd525-…", "attributes": { "title": "Spring launch recap" } },
    { "type": "node--article", "id": "08d88ef2-…", "attributes": { "title": "Designing for headless from day one" } },
    { "type": "node--article", "id": "36579530-…", "attributes": { "title": "Q3 platform roadmap highlights" } },
    { "type": "node--article", "id": "ba1f4fb1-…", "attributes": { "title": "How editorial workflows changed our release cadence" } }
  ],
  "meta": { "count": 4 },
  "links": { "self": { … } }
}
```

### Canonical form (any operator)

```
?filter[LABEL][condition][path]=FIELD_PATH
&filter[LABEL][condition][operator]=OPERATOR
&filter[LABEL][condition][value]=VALUE
```

`LABEL` is any name you choose for the condition. The condition keys:

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `[condition][path]` | string | yes | none | Field path, machine names only; dot notation follows relationships (`tags.name`) |
| `[condition][operator]` | string | no | `=` | One of the [operators below](#operators), exactly as written |
| `[condition][value]` | string or array of string | all operators except `IS NULL`/`IS NOT NULL` | none | The comparison value; multi-value operators take repeated `value[]` entries |
| `[condition][memberOf]` | string | no | none | Attaches the condition to a named [filter group](#filter-groups-andor) |

Example: titles containing "launch":

```
GET /api/node/article?filter[t][condition][path]=title
  &filter[t][condition][operator]=CONTAINS
  &filter[t][condition][value]=launch
  &fields[node--article]=title
```

```jsonc
{
  "jsonapi": { "version": "1.1", "meta": { … } },
  "data": [
    { "type": "node--article", "id": "7f7dd525-…", "attributes": { "title": "Spring launch recap" } }
  ],
  "meta": { "count": 1 },
  "links": { "self": { … } }
}
```

### Operators

| Operator | Matches | Example (condition parts) |
|---|---|---|
| `=` | Exact value (the default; the short form uses it) | `path=status`, `operator==`, `value=1` |
| `<>` | Anything except the value | `path=status`, `operator=<>`, `value=0` |
| `>` | Greater than | `path=created`, `operator=>`, `value=1893456000` (a timestamp; see the date caveat below) |
| `>=` | Greater than or equal | `path=field_price`, `operator=>=`, `value=10` |
| `<` | Less than | `path=created`, `operator=<`, `value=1893456000` (a timestamp; see the date caveat below) |
| `<=` | Less than or equal | `path=field_price`, `operator=<=`, `value=100` |
| `STARTS_WITH` | String prefix | `path=title`, `operator=STARTS_WITH`, `value=How` |
| `CONTAINS` | Substring | `path=title`, `operator=CONTAINS`, `value=release` |
| `ENDS_WITH` | String suffix | `path=title`, `operator=ENDS_WITH`, `value=2026` |
| `IN` | Any of several values, pass `value[]` multiple times | `path=tags.name`, `operator=IN`, `value[]=news&value[]=events` (each entry repeats the same `filter[...]` prefix; full example below) |
| `NOT IN` | None of several values | `path=tags.name`, `operator=NOT IN`, `value[]=archive` |
| `BETWEEN` | Inside a range, exactly two `value[]` entries | `path=field_price`, `operator=BETWEEN`, `value[]=10&value[]=100` |
| `NOT BETWEEN` | Outside a range | `path=field_price`, `operator=NOT BETWEEN`, `value[]=10&value[]=100` |
| `IS NULL` | Field has no value (omit `value`) | `path=field_subtitle`, `operator=IS NULL` |
| `IS NOT NULL` | Field has any value (omit `value`) | `path=field_subtitle`, `operator=IS NOT NULL` |

**Date comparisons use UNIX timestamps (verified).** `created` and `changed` are stored as timestamps, and comparison operators compare against the raw stored value, even though responses display ISO 8601. `filter[t][condition][value]=2030-01-01` silently compares against the integer `2030` (matching almost nothing, or everything, depending on direction); `filter[t][condition][value]=1893456000` compares against the date you meant. Convert dates to timestamps before filtering: `date -d 2030-01-01 +%s` (GNU) or `date -j -f %Y-%m-%d 2030-01-01 +%s` (macOS).

Full `IN` example:

```
GET /api/node/article?filter[tags][condition][path]=tags.name
  &filter[tags][condition][operator]=IN
  &filter[tags][condition][value][]=news
  &filter[tags][condition][value][]=events
```

### Filter groups (AND/OR)

Multiple conditions are ANDed by default. To OR them, declare a group with a `conjunction` and attach conditions to it with `memberOf`:

```
GET /api/node/article?filter[or-group][group][conjunction]=OR
  &filter[a][condition][path]=title
  &filter[a][condition][operator]=CONTAINS
  &filter[a][condition][value]=beta
  &filter[a][condition][memberOf]=or-group
  &filter[b][condition][path]=title
  &filter[b][condition][operator]=CONTAINS
  &filter[b][condition][value]=preview
  &filter[b][condition][memberOf]=or-group
```

This matches articles whose title contains "beta" OR "preview". Groups can nest: a group itself takes `memberOf` to join a parent group. The [JSON:API Query Builder](/start-here/glossary/#jsonapi-query-builder) (the interactive query workbench in your site's admin UI at `API > JSON:API Query Builder`) generates these in its `Filter Groups (AND/OR logic)` section.

Used in: [querying guide → filtering](/source-cms/content-api/guide/).

## `fields[TYPE]`: sparse fieldsets

Return only the named fields for a resource type. The type key is the full resource type, `{entity_type}--{bundle}`:

```
GET /api/node/article?
  fields[node--article]=title,created,image
```

- Fields not listed are omitted from both `attributes` and `relationships`.
- Repeat the parameter per type to trim included resources too: `&fields[media--image]=media_image`.
- Smaller responses are faster; the Query Builder's `Fields` tab builds this parameter.

Each entry's `attributes` carries only the named field, and `relationships` is gone entirely:

```jsonc
{
  "jsonapi": { "version": "1.1", "meta": { … } },
  "data": [
    { "type": "node--article", "id": "7f7dd525-…", "links": { … },
      "attributes": { "title": "Spring launch recap" } },
    …
  ],
  "meta": { "count": 4 },
  "links": { "self": { … } }
}
```

Used in: [querying guide → sparse fieldsets](/source-cms/content-api/guide/).

## `include`: related resources in one request

Embed related entries in the response's top-level `included` array instead of fetching them one by one:

```
GET /api/node/article?include=image
```

- Comma-separate multiple relationships: `include=image,tags`.
- Dot notation follows relationships through intermediate entities, e.g. through a media entity to its file: `include=image.media_image`.
- No nesting-depth cap is documented, and none was encountered in verification (two-level chains like `image.media_image` work; each segment must be a relationship field, or the request 400s naming the failing segment, see [400 responses](#400-responses-for-malformed-queries)).

For the two-level chain `GET /api/node/article?include=image.media_image&page[limit]=1`, every entity on the chain lands in `included`, once each:

```jsonc
{
  "jsonapi": { "version": "1.1", "meta": { … } },
  "data": [
    { "type": "node--article", "id": "7f7dd525-…",
      "attributes": { … },
      "relationships": { "image": { "data": { "type": "media--image", "id": "5f2c5cde-…" } }, … } }
  ],
  "included": [
    { "type": "media--image", "id": "5f2c5cde-…", "attributes": { "name": "Portal verification hero", … } },
    { "type": "file--file", "id": "0d28b1dd-…",
      "attributes": { "filename": "portal-verification-hero.png",
                      "uri": { "url": "/sites/default/files/2026-07/portal-verification-hero.png", … }, … } }
  ],
  "meta": { … }, "links": { … }
}
```

Used in: [querying guide → relationships and the N+1 pitfall](/source-cms/content-api/guide/).

## `page[limit]` and `page[offset]`: pagination

```
GET /api/node/article?page[limit]=10&page[offset]=20
```

| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| `page[limit]` | integer | no | 50 | Maximum number of entries in one response; values above 50 are clamped to 50 (verified). Lower limits make the server respond faster |
| `page[offset]` | integer | no | 0 | Number of entries to skip before the first returned entry |

Responses include a [`links.next`](/source-cms/reference/response-document/#the-success-document) URL while more entries remain. Follow it rather than computing offsets by hand. It adds the `page[offset]` and preserves every other parameter, with square brackets percent-encoded. The shape for `GET /api/node/article?page[limit]=2&sort=-created` on a five-entry collection:

```json
{
  "meta": { "count": 5 },
  "links": {
    "next": {
      "href": "https://your-site.example.com/api/node/article?page%5Boffset%5D=2&page%5Blimit%5D=2&sort=-created"
    }
  }
}
```

`meta.count` is the collection total. Caveat: pagination links are computed **before** access filtering, so a page can return `"data": []` and still carry `links.next`/`links.last` (for example when every entry on it is unpublished). Don't treat an empty page as the end of the collection; follow `links.next`, or check `meta.count` and `meta.omitted` (see [omitted resources](/source-cms/reference/content-api/#omitted-resources)).

Used in: [query quickstart](/source-cms/content-api/quickstart/) (`page[limit]` is its one variation), [querying guide → pagination](/source-cms/content-api/guide/).

## `sort`: result order

```
?sort=FIELD_PATH
```

| Syntax | Meaning |
|---|---|
| `sort=created` | Ascending by `created` |
| `sort=-created` | Descending: prefix the path with `-` |
| `sort=-status,created` | Multiple keys, comma-separated, applied left to right |
| `sort=tags.name` | Dot notation sorts by a related entry's field |

Example: alphabetical by title:

```
GET /api/node/article?sort=title&fields[node--article]=title
```

```jsonc
{
  "jsonapi": { "version": "1.1", "meta": { … } },
  "data": [
    { "type": "node--article", "id": "08d88ef2-…", "attributes": { "title": "Designing for headless from day one" } },
    { "type": "node--article", "id": "ba1f4fb1-…", "attributes": { "title": "How editorial workflows changed our release cadence" } },
    { "type": "node--article", "id": "36579530-…", "attributes": { "title": "Q3 platform roadmap highlights" } },
    { "type": "node--article", "id": "7f7dd525-…", "attributes": { "title": "Spring launch recap" } }
  ],
  "meta": { "count": 4 },
  "links": { "self": { … } }
}
```

Used in: [querying guide → sorting](/source-cms/content-api/guide/).

## Language

The content language is selected per request with the `langCode` query parameter, provided by the [JSON:API Multilingual module](https://www.drupal.org/project/jsonapi_multilingual), which every Source CMS site ships active (confirmed 2026-08-19; behavior verified live the same day). Selection is strict: a missing translation is a `404` whose error object names the entry's viewable translations in `errors[0].detail` and, machine-readably, in `errors[0].meta.availableTranslations` (captured: `The "fi" translation of the specified resource does not exist. Available translations: en.`). A language not enabled on the site is a `422` (captured: `The specified language ("de") is invalid or has not been configured.`).

Adding `includeFallback=1` resolves the read through the site's language fallback chain instead, so an existing entry always returns `200`. It is valid only together with `langCode`, only on reads, and only with the literal value `1`; the rejections are `400`s (captured: `The "includeFallback" query parameter requires the "langCode" query parameter.` and `The "includeFallback" query parameter only accepts the value "1".`).

Both parameters apply to individual, collection, and `related` reads and, for `langCode`, to writes (reads and writes verified 2026-08-19). The language actually served is reported by the `Content-Language` response header (individual responses) and each entry's `langcode` attribute. See the [multilingual guide](/source-cms/content-api/multilingual/) for requesting translations and detecting fallback, and [translating content](/source-cms/content-api/translating-content/) for the per-translation writes. A site without the module (a local stand-in, say) accepts `langCode` without acting on it, the treatment JSON:API's parameter naming rules prescribe for implementation-specific parameters a server does not implement.

A [langcode](/start-here/glossary/#langcode) URL path prefix (for example `/es/api/node/article`) resolves for enabled languages and 404s otherwise. Whether it selects the content language is site negotiation configuration, not a contract: a local product instance negotiated by prefix (2026-07-03), while the Source CMS site verified 2026-08-19 returned the default-language entry under a prefix even when the translation existed. Select with `langCode`, not the prefix. `filter[langcode]` filters by translation existence without changing the language served (verified 2026-08-19: `filter[langcode]=fi` matched an entry with a Finnish translation and returned it in the default language).

A lowercase `?langcode=` is rejected with `400` and the literal detail `The following query parameters violate the JSON:API spec: 'langcode'.` (verified 2026-07-03 and 2026-08-19), like any parameter that violates JSON:API's naming rules, `lang` included (see [400 responses](#400-responses-for-malformed-queries)). The `Accept-Language` request header does not negotiate content language (verified 2026-07-03 and 2026-08-19). Each entry's language appears verbatim as the `langcode` attribute in responses.

## 400 responses for malformed queries

A malformed query returns `400 Bad Request` with a JSON:API error document:

```json
{
  "jsonapi": { "version": "1.1" },
  "errors": [
    {
      "title": "Bad Request",
      "status": "400",
      "detail": "..."
    }
  ]
}
```

The `errors[].detail` member names what was wrong. Causes mapped to the literal `detail` (JSON:API 1.1):

| Cause | Literal `detail` | Fix |
|---|---|---|
| Unknown field path in `filter` or `sort` | `` Invalid nested filtering. The field `nonexistent_field`, given in the path `nonexistent_field`, does not exist. `` | Use the field's machine name, not its label; the Query Builder's `Field` dropdown lists valid paths |
| Invalid or misspelled `operator` | `The 'LIKE' operator is not allowed in a filter parameter.` (the accepted set is not listed in the error) | Use one from the [operators table](#operators), exactly, uppercase where shown |
| `include` path that isn't a relationship | `` `uid.bad.deep` is not a valid include path. `bad` is not a valid relationship field name. Possible values: roles. `` (the error lists the valid relationship names at the failing segment) | Only relationship fields can be included; attributes cannot |

A non-numeric `page[limit]` or `page[offset]` does **not** return a 400. The invalid value is silently ignored and the default applies. If your limit doesn't seem to take effect, check its spelling and value before suspecting the data.

An unknown all-lowercase query parameter violates JSON:API's parameter naming rules and is rejected (verified): `` The following query parameters violate the JSON:API spec: 'cb'. `` A name carrying a non-lowercase character (`langCode`, say) is an implementation-specific parameter under those same rules: it passes validation and does nothing unless something on the site implements it (see [Language](#language)). Either way there is no cache-busting via made-up parameters.

Used in: [querying guide → troubleshooting](/source-cms/content-api/guide/).

## Used in

- [Content API quickstart](/source-cms/content-api/quickstart/): `page[limit]`
- [Querying guide](/source-cms/content-api/guide/): every parameter above
- [Multilingual content guide](/source-cms/content-api/multilingual/): language handling
- [Translating content](/source-cms/content-api/translating-content/): per-translation writes with `langCode`
- [Creating & updating content via the API](/source-cms/content-api/writing-content/): reading back what you write
