Skip to content

Every response the Content API returns is one JSON:API document with the same envelope. Each member is explained below, with the matching slice of a live response pinned alongside it. For the endpoints that produce these documents, see the Content API reference; for the query parameters that shape them, see the query parameter reference.

The slices below come from one response to GET /api/node/article?include=image,tags&page[limit]=1.

jsonapi: always present. The spec version the document conforms to.

"jsonapi": {
"version": "1.1",
"meta": { … }
}

data: the requested resource(s). Its shape follows the endpoint:

Request data is
A collection (/api/node/article) an array of resource objects
A single resource (/{uuid}) one resource object
A relationship pointing at nothing null

Each resource is identified by type + id together, the same pair used in data, in included, and inside relationship linkage. A document is a success or a failure, never both: data and errors never appear together.

"data": [
{
// type + id identify any resource, everywhere.
"type": "node--article",
"id": "7f7dd525-fa49-465e-976d-a50ab5425c49",
"links": { "self": { "href": "…/api/node/article/7f7dd525-…" } },
"attributes": { … },
"relationships": { … }
}
]

data[].attributes: every non-reference field of the entry, keyed by machine name. The field-type table maps each type to its JSON shape.

"attributes": {
"langcode": "en",
"status": true,
"title": "Spring launch recap",
"created": "2026-07-02T14:51:56+00:00",
"moderation_state": "published",
"body": {
"value": "The spring launch shipped on time…",
"format": "filtered_html",
"processed": "<p>The spring launch shipped on time…</p>"
}
}

data[].relationships: every reference field, keyed by machine name. Each holds resource identifiers (type + id), never the referenced entries themselves:

Cardinality data is
One-to-one one identifier, or null
One-to-many an array of identifiers

The entries themselves arrive in top-level included when the request adds ?include=.

"relationships": {
// one-to-one: "data" is one identifier (or null)
"image": {
"data": { "type": "media--image", "id": "5f2c5cde-…" }
},
// one-to-many: "data" is an array of identifiers
"tags": {
"data": [
{ "type": "taxonomy_term--tags", "id": "dbc476ff-…" },
{ "type": "taxonomy_term--tags", "id": "19059886-…" }
]
}
}

included: the referenced entries themselves, present only when the request used ?include= and at least one named relationship resolved to an entity; an include that matches nothing (every relationship empty) omits the key entirely rather than sending [], so read it as doc.included ?? []. Each entry:

  • has the same type / id / attributes / relationships shape as a data resource,
  • is deduplicated: one entry per resource, however many relationships point to it,
  • is matched back to a relationship by its type + id.
"included": [
{
"type": "media--image",
"id": "5f2c5cde-…",
"attributes": { "name": "Portal verification hero", … }
},
{
"type": "taxonomy_term--tags",
"id": "dbc476ff-…",
"attributes": { "name": "news", … }
}
]

meta.count: collection responses only, the total number of matching entries, counted before pagination and before access filtering. It can therefore exceed what data actually contains (see the empty-data case below).

"meta": { "count": 4 }
"links": {
"self": { "href": "…/api/node/article?…" },
"next": { "href": "…/api/node/article?page%5Boffset%5D=1&…" },
"last": { "href": "…/api/node/article?page%5Boffset%5D=3&…" }
}

When data is empty but the collection is not

Section titled “When data is empty but the collection is not”

When access control hides entries (most commonly unpublished ones), the response stays 200 and signals the gap in meta. The slices below are a separate capture of the article collection (a different snapshot than the one above, hence the different meta.count), on a page whose two newest entries are unpublished.

data: []: nothing visible on this page, but an empty page is not the end of the collection: links.next still points onward, so keep following it.

"data": [],
"links": {
"self": { … },
"next": { … },
"last": { … }
}

meta.omitted: meta.count (7) disagrees with data.length on purpose: count is the whole collection, data is only what you may see. omitted.links names why each hidden resource is missing, one link per entry.

"meta": {
"count": 7,
"omitted": {
"detail": "Some resources have been omitted because of insufficient authorization.",
"links": {
"help": { "href": "https://www.drupal.org/docs/8/modules/json-api/filtering#filters-access-control" },
"item--IYrGUC8": {
"href": "…/api/node/article/c59ab903-…",
"meta": {
"rel": "item",
"detail": "The current user is not allowed to GET the selected resource. The 'view any unpublished content' permission is required."
}
}
}
}
}

On failure, errors replaces data entirely. Here, a filter on a field that doesn’t exist returns 400 Bad Request:

errors: replaces data on failure; one object per error, every member a string:

Member Holds
detail the diagnostic: names exactly what was wrong (read this first)
title the generic status name, e.g. "Bad Request"
status the HTTP status as a string, e.g. "400"
links via (the offending request) and info (the spec for that status)
DetailsContent API reference → error codes for the shared table, authentication reference for 401/403, and query parameter reference → 400 responses for malformed queries
"errors": [
{
"title": "Bad Request", // generic name of the status code
"status": "400", // the HTTP status, as a string
// the member to read: names exactly what was wrong
"detail": "Invalid nested filtering. The field `nonexistent_field`, given in the path `nonexistent_field`, does not exist.",
"links": {
"via": { "href": "…/api/node/article?filter…" },
"info": { "href": "https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1" }
}
}
]

Was this page helpful?