Content Cloud GraphQL Content API
Introduction
The Content Cloud GraphQL Content API provides a GraphQL interface to query the content in your Content Cloud spaces. Every Space’s content model is translated into a GraphQL schema on the fly, so it always reflects the latest structure of your content types. This means you can query all your content types, entries, and assets via GraphQL with types and fields corresponding to your content model.
Using GraphQL, you can request exactly the data you need in a single request, including traversing references, which can be more efficient for many use cases. The GraphQL API can serve both published (live) content and preview (draft) content, depending on the arguments and access tokens you use.
Content Cloud APIs are mostly compatible with Contentful. You can read more about Contentful compatibility here.
Note: You will need an active Content Cloud account with at least one Space configured (and your source site/service content synchronized) to use this API. Ensure you have your Space ID and a valid API access token before proceeding.
Getting started: The recommended way of adding Content Cloud to your project is by using our auto-generated clients. This will provide the content model as a schema in your frontend and allow you to spend less time writing repetitive integration code.
Requirements
Before you can use the Delivery APIs, you first have to feed content into Content Cloud. You can do so by creating a Space in the Content Sync backend for your source site or services, e.g. Drupal. After connecting your source sites and services, you can push content to Content Sync. To serve the pushed content through the Delivery API, you have to create a second Content Cloud space and connect the two. Please refer to the general Onboarding instructions to learn more.
Basic API Information
API Base URLs: The base URL for the GraphQL Content API depends on the environment of content you want to access. The following endpoints are available:
CDN Content API: e.g.,
https://{ENVIRONMENT_ID}.cdn.cloud.content-sync.io/{VERSION}/graphql, for published content via CDN.Live Content API: e.g.,
https://{ENVIRONMENT_ID}.live.cloud.content-sync.io/{VERSION}/graphql, for published content, uncached.Preview Content API: e.g.,
https://{ENVIRONMENT_ID}.preview.cloud.content-sync.io/{VERSION}/graphql, for previewing content including drafts/unpublished entries.
In the above, replace {ENVIRONMENT_ID} with your actual Space ID, and optionally include the Environment ID. The exact domain and for your Space’s API endpoints can be found in your Content Cloud space settings. The {VERSION} is used for API versioning (see below).
The GraphQL API is read-only for content (no mutations for content management). All queries should be sent as HTTP GET or POST requests to this endpoint.
API versioning
Content Cloud changes to the API are backwards-compatible by default. You can pass a version parameter for every request as part of the path (see {VERSION} above). This version can either be latest, meaning you will receive responses based on the latest spec, or a date in the format YYYY-MM-DD to use the API format of the spec from this date, 0:00:00 am UTC. For example:
https://{ENVIRONMENT_ID}.cdn.cloud.content-sync.io/latest/graphqlto use the latest API spec.https://{ENVIRONMENT_ID}.cdn.cloud.content-sync.io/2025-09-19/graphqlto use the API spec from September 18, 2025 at 23:59:59.999 (UTC).
It’s usually a good idea to use the date of your development start for the version parameter and update it routinely to use newer specs.
Content Cloud APIs are backwards-compatible for at least 1 year, and breaking changes from backwards-compatibility reaching their EOL are communicated in advance.
Auto-generated clients will always use the API version of the date the client code was generated.
HTTP Methods
The GraphQL API supports both POST and GET for executing queries:
POST is recommended for most use cases, especially for complex queries or if you are sending a GraphQL query in the request body. It can be more secure, as the query isn’t in the URL and allows larger queries than GET (since URLs have length limits).
GET can be used for simple queries, where you URL-encode the query and include it as a query parameter (and optionally variables parameter) in the URL.
Content Cloud will optimize queries at the edge already and cache the response when using the cdn service regardless of the HTTP method used, so GET requests will not provide a better performance.
For example, a GET request might look like:
(The above is a URL-encoded query of { articleCollection { items { title } } }.)
Use POST for bigger queries: Content Cloud imposes a max URL length / query size for GET of ~2048 characters. Using POST avoids those limits and is generally easier to work with.
Example Query and Usage
Throughout these docs, we’ll use a running example of querying a blog posts content. Suppose we have a content type “Blog” with a field title. A simple GraphQL query to get all blog titles could be:
You can send this via POST by putting it in the JSON body:
If the query has variables or operation name, include those JSON fields too. You can read more about the GraphQL standard below or at their official website.
POST Requests
When using POST, set the request headers and body appropriately:
Content-Type: application/json
Authorization: Bearer {ACCESS_TOKEN}
Request Body: A JSON object with:
"query": containing the GraphQL query string (which can be a multiline string or all on one line)."variables"(optional): an object of any GraphQL variables your query uses."operationName"(optional): if you have multiple operations in your query and want to specify which one.
Example POST (cURL):
This example sends a query to fetch blog titles.
GET Requests
GET requests pass the GraphQL query and variables via query string parameters:
query=<urlencoded GraphQL query>variables=<urlencoded JSON object>
Remember to URL-encode the query (including special characters, spaces, etc.) or use a library to construct the URL.
Example GET (cURL):
This is equivalent to asking for all blog titles (as above). For complex queries, this gets unwieldy, so use POST if possible.
Authentication
By default, any client (application or user agent) requesting content from the Content Cloud Delivery API must provide a valid access token with the request. The access token authenticates your request and determines which Space and Environment you can access. You can supply the token as:
HTTP Header: Provide the token via the
Authorizationheader as a Bearer token. For example:Authorization: Bearer MY_ACCESS_TOKEN
The access token you use must have access to the target space and environment. For instance, if a token is limited to the “live” environment of a space, it cannot be used to fetch content from the “preview” environment or a different space.
Access tokens are generated using client secrets. To generate a secret, open the project settings, navigate to Authorization (clients), create a client and then a secret. Copy-paste the result and save it to your project as an environment variable. Keep your tokens secure, i.e., treat them like passwords and do not expose them in client-side code.
Use the secret to generate access tokens on-demand or create a long-lived token that’s used in your applications. Access tokens must have a space, one or more environments, allowed APIs, and permissions assigned. You can read more about access here.
Tip: Use the auto-generated clients and their helper functions to generate tokens more easily. You can create different tokens, e.g., for live published content and for preview content.
API Rate Limits
GraphQL requests count against your API rate limit of 50 requests/second by default for uncached GraphQL queries.
If the rate limit is exceeded, you get HTTP 429 Too Many Requests. The response will include X-RateLimit-Reset: 1 header indicating how many seconds to wait (usually 1 second).
Even though GraphQL can fetch more data in a single request, each request is what is counted (not each field). So design your usage accordingly:
Prefer one larger GraphQL query rather than many small ones in parallel, to reduce total request count.
However, very large queries have complexity limits (see next section), so find a balance.
Very large queries can also be slower, so consider loading data in multiple steps if, e.g., some content is displayed immediately in a mobile app and some data is fetched for later display.
Example of rate limit: 85 GraphQL requests in one second will lead to some requests being rejected with 429 and the reset header telling you to wait 1 second. Just like when using the REST Delivery API, implement retry with backoff when you hit 429.
Query Complexity Limits
To prevent excessively expensive queries, the GraphQL API enforces a query complexity limit. Complexity roughly corresponds to the number of entities (entries or assets) that could be returned by a query.
By default, a single GraphQL query can return up to 11,000 entities (entries/assets) in total. The complexity calculation considers the worst-case count of items a query could retrieve.
For example:
Querying a collection with
limit: 20has complexity 20 (could return 20 entries).If each of those entries has a multi-reference field and you query 10 linked items per entry, that part adds up to 20 * 10 = 200 potential items, so complexity ~220.
If you have nested queries with unions/fragments, those can multiply out.
In practice:
A simple query for 1 entry = complexity 1. 100 entries = complexity 100.
100 entries each with 5 assets loaded = complexity 600 (100 entries + 500 assets).
The default maximum complexity is 11,000. If you need more, it might require special plan increase.
If you exceed the complexity limit, the API will return an error indicating the query is too complex and was denied.
Examples:
Complexity: 20 (can return up to 20 Lessons).
If each Lesson has up to 10 images, that’s 20 lessons + 200 images = 220 complexity.
The GraphQL response includes an HTTP error (400 or 422 status likely) if complexity is too high, with a message about exceeding complexity.
Reducing complexity: Use pagination (limit smaller, or query fewer items at a time), or fetch less nested data per query if possible. You can also omit unnecessary fields.
Query Size Limits
The GraphQL query text itself has a size limit (to prevent extremely large query strings). By default, the maximum query text size is 8 KB for Content Cloud.
This includes the JSON formatting. Whitespace and comments count toward this, so removing unnecessary spaces/newlines can help if you’re near the limit.
Rich Text
If your content model includes Rich Text fields (long-form content with formatting, which in Content Cloud is a special rich text document JSON), the GraphQL API represents them with a specific structure.
A Rich Text field in GraphQL is typically typed as an object with two sub-fields:
json: The raw rich text content in JSON form.links: A nested structure of any entries or assets embedded within the rich text, organized by type.
For example, in the schema you might see:
And MyRichTextFieldLinks might contain fields like entries and assets with nested collections of the linked items.
When you query a rich text field, you can either retrieve the json (which you then render with a Rich Text renderer in your app), or you can also pull some info about linked entries (like their titles or URLs) via the links sub-selection.
Complexity and limits: Each embedded link in rich text counts toward query complexity. The default is that up to 1000 linked entities in a single Rich Text field will be resolved. If you have more than 1000 embedded links in one rich text, it likely won’t resolve all of them.
If you only need the raw rich text, you can just query the json property and not incur the cost of resolving links at that time (you could fetch them separately if needed).
Source: Rich text compatible content must be provided by the source site or system, e.g., by using the prosemirror module in Drupal.
Tags
In GraphQL, each entry and asset type includes a sys.metadata field which contains an array of tags.
And every entry type has:
This means you can retrieve tags on an entry by querying, for example:
This will list the tags applied to each article.
You can also filter by tags in GraphQL using the where filters (see below: filtering by sys.metadata.tags).
Previewing Content (Drafts in GraphQL)
By default, queries to the GraphQL API will return published content (from the delivery environment). To retrieve draft (unpublished) content via GraphQL, you typically have two requirements:
Content Cloud requires you to use the preview endpoint to access preview content (see above for service URLs).
With a preview-enabled token, your queries will return entries that are not published (drafts). If an entry exists in draft and published states, the draft version is returned.
On single entry queries (like blog(id: "123")), you can also fetch the draft version using the preview API endpoint.
If you use the cdn or live API endpoints, you get only published content. If you use a delivery token (no preview permission), using the preview API will cause a permissions error.
Important: When previewing content, note that draft entries might reference other draft entries or assets. The GraphQL API will resolve linked entries in preview mode as well (assuming those entries are accessible with the token).
Also, the sys metadata of a draft entry might not have a publishedAt and publishedVersion property.
Reference (GraphQL Schema and Usage Details)
In this section, we explain how your content model is translated into the GraphQL schema and how to use various features like filtering, ordering, and special field types. Knowing these details can help you craft efficient and powerful queries.
Locale Handling in GraphQL
GraphQL queries by default return content in the default locale of the space (just like REST). If you want content from a different locale, you have to provide it as a parameter in the request URL.
If a locale falls back to another and you request a locale where the entry has no value:
The API might return null for an entry if it strictly doesn’t have content in that locale.
The API might return content in a fallback locale if it exists in that locale.
Schema Generation
The Content Cloud GraphQL schema is generated from your content types. Here’s how things map:
Content Types -> GraphQL Types: Each content type in your space results in a corresponding GraphQL object type. The GraphQL type name is a PascalCase version of the content type’s name or machine name. For example, a node type with the machine name
articlein Drupal becomes a typeArticleContentin the schema. If your content type name has spaces or special characters, those are removed or normalized to ensure the type name is a valid GraphQL identifier and unique. In rare cases of conflicts, it may adjust names or throw an error – see Reserved Names below.
Fields -> GraphQL Fields: Each field of a content type becomes a field on the GraphQL type:
Simple scalar fields like Text, Symbol, Number, Boolean, Date map to GraphQL scalar types: String, Int, Float, Boolean, and DateTime (DateTime is typically used for ISO 8601 timestamps).
Location fields become a Location object type with lat and lon Float fields.
Rich Text fields become a type with json and links (as described in Rich Text section).
Reference fields (links to entries or assets) become either GraphQL object fields or more complex structures depending on their validation; see Modeling Relationships below.
Collections -> Collection Types: For each content type, the schema also defines a collection type, used for paging through multiple entries. This type is typically named <TypeName>Collection and contains:
items: [TypeName]: an array of the entries.total: Int: total number of items (matching the filter, if any).limit: Int: the maximum number of items returned (as provided in the query).skip: Int: how many items were skipped (offset).
For example, for BlogPost we have BlogPostCollection with those fields.
Query Entry Points: The schema’s Query type will have fields to query your content:
A plural-collection field for each content type, usually named like <contentTypeId>Collection. E.g., blogPostCollection to query a list of BlogPost entries.
A single entry field for each content type, named as the content type (or something similar). E.g., blogPost(id: String!) to fetch one BlogPost by ID.
Also, a Query field for assets:
assetCollection to list assets (with filtering by MIME type, etc.).
asset(id: String!) to get one asset by ID.
For example, you might have:
Using these, you can query entries of a type or specific entry by ID.
Reserved Type Names & Conflicts: Some names are reserved or could conflict:
GraphQL system types (Query, Mutation, Subscription, etc.) cannot be used as content type names.
If two content types would result in the same GraphQL type name, the generation will adjust or fail. For instance, content types named "Friendly User" and "FriendlyUser" could both map to FriendlyUser type, which is a conflict. The system might append a suffix or use the internal ID instead to differentiate, or it may throw a schema error telling you to rename one of them.
This will fail during the synchronization of the content model from the source system as all content types must have a unique machine name assigned that can be used in GraphQL.
Similarly, if a content type name collides with an automatically generated helper type (like a collection or union type name), the schema generation might fail. A content type named
PlantsOrdermay conflict with the helper type for ordering aPlantstype.
Best practice: ensure your content type IDs are unique and do not differ only by special characters or casing. The system will handle normal situations, but if you encounter a schema generation error, it likely means a naming conflict that needs resolution by renaming a content type or field.
Sys Field: Every entry type in GraphQL gets a field sys which is an object of type Sys containing system metadata. This includes:
id: String!: the entry’s unique ID.spaceId: String!: the space ID.environmentId: String!: the environment ID.publishedAt: DateTime: timestamp when this entry was last published (or null if never).firstPublishedAt: DateTime: timestamp when this entry was first published (or null if never).publishedVersion: Int: the version number of this entry at the time of publish (if relevant).
Notice that createdAt and updatedAt (which exist in REST sys) are not present here as GraphQL focuses on publish times. Essentially, in GraphQL publishedAt (last published) corresponds to what the Delivery API calls sys.updatedAt for published entries. If you have content that is not yet published, these fields will be null. If you need the draft’s last saved time, GraphQL doesn’t expose that directly; you would see changes only once published. If you query from the preview API, the publishedAt remains the last publish time, not the draft save time.
You can query sys like any other sub-selection:
sys.metadata Field: As mentioned in the Tags section, each entry and asset type also has a sys.metadata: EntryMetadata field. This contains the tags array (list of tags with id and name) for that entry. If no tags, the list is empty. You can filter by tags via this field (explained in Filters section).
Modeling Relationships (References): Perhaps the trickiest part is how reference fields (links to other entries/assets) appear in GraphQL. It depends on the cardinality and type constraints of the link:
One-to-One (Single Reference, Single Content Type): If a field is a single link to one entry and the content model restricts it to a specific content type (via validation), then the GraphQL field will simply be that type. For example, if
Articlehas a fieldauthorthat links to an entry of content typeAuthor(and onlyAuthor), then in GraphQL:type Article { author: Author ... }So you can directly query
article.author.name, etc. The field can benullif not set. No union or special handling needed since the type is known and fixed.
One-to-One (Single Reference, Multiple Content Types): If a single link field can reference different content types (the validation allows multiple or is not restricted), then GraphQL represents it as a union type. For example, suppose
ContentBlockhas a fieldlinkedItemthat could be either anArticleor aProduct. GraphQL will create a union, e.g. unionContentBlockLinkedItem = Article | Product, and the field becomes:type ContentBlock { linkedItem: ContentBlockLinkedItem }When you query this content,
linkedItemmight be one of those types. You’ll have to use inline fragments to get specific fields depending on the actual type (see below). The field could also benull(no link).
One-to-Many (Array, Single Content Type): If a field is an array of links (a list of entries) and it’s restricted to one content type, the GraphQL schema will expose it as a Collection of that type. For example, an
Authorcontent type might have a multi-reference fieldbooksCollectionlinking to multiple Book entries. In GraphQL:type Author { booksCollection: BookCollection }Notice it returns a
BookCollectiontype. This is done to allow pagination on that field and to follow GraphQL best practices for lists. You can then query forauthor.booksCollection.itemsand page through if needed.This collection field supports arguments: e.g., you can do
author(id:"123") { booksCollection(limit: 5, where: {...}) { items { title } } }. If you don’t provide a limit, a default (100) is used. The total field inside can tell you how many books in total that author has.
One-to-Many (Array, Multiple Content Types): If an array field can contain different content types, the schema will still use a collection but the items within that collection will be a union of possible types. For example, say
Dashboardhas a field widgets that can be a list of entries of type eitherChartorTextBlock. GraphQL might define:union DashboardWidgetsItem = Chart | TextBlock type DashboardWidgetsCollection { items: [DashboardWidgetsItem]! total: Int! skip: Int limit: Int } type Dashboard { widgetsCollection: DashboardWidgetsCollection }Here,
dashboard.widgetsCollection.itemsis a list of union typeDashboardWidgetsItemwhich could beChartorTextBlock. You’ll again use inline fragments when querying to handle the different types.
Asset links: If a field is a link to an
Asset(e.g., an image field), that will appear simply as typeAsset(or[Asset]for multiple).For single asset:
type Product { image: Asset }For multiple assets:
type Gallery { photosCollection: AssetCollection }(Similar to entries, they use an AssetCollection wrapper for multi-asset fields.)
Inline vs Block references: This concept is more for Rich Text (where “inline” and “block” entries can be embedded in text). Outside of Rich Text, references are just fields as above. So no need to worry unless dealing with rich text links.
Inline Fragments (for Unions): Whenever you have a union type (multi-type reference), you need to use GraphQL’s inline fragment syntax to get fields of the specific type. You can also query the __typename field to see which type was returned at runtime.
Example: Suppose ContentBlock.linkedItem can be Article or Product. A query might look like:
The __typename will tell you if it’s "Article" or "Product" for each item. Within each fragment, you can query fields specific to that type. Fields not on that type will be ignored for other types.
Likewise for unions in collections:
Each item in widgetsCollection.items will yield either Chart or TextBlock fields.
If a union type has many possibilities, include fragments for the ones you care about. Any types you omit will simply not return their specific fields (but you can still get __typename).
Reference Field Nullability: GraphQL types for references are typically nullable by default (e.g., author: Author not Author!), because the link might not be set. Also, if a linked entry is deleted or unpublished (and you’re not using the preview endpoint), the API might return null for that reference.
Entries and Assets Types
Entries: Each content type’s GraphQL type includes all its content fields, plus the metadata fields at sys. It does not include linked entries’ fields directly (you navigate those via the link fields). The type name is usually TitleCased from the content type name.
For example, if you’re using Drupal as the source site/system with a node type node.event, you will have an EventContent type by default in GraphQL with fields like title and body (plus sys).
Assets: The GraphQL type for an asset is Asset. Asset fields include:
sys: Sys!(with id, etc., similar to entries)File information fields:
url(transform: ImageTransformOptions): Returns a URL string for the asset file, optionally transformed if it’s an image.title: String: the title of the asset (often used as alt text for images).description: String: description of the asset.contentType: String: the mime type of the asset.fileName: String: the actual file name.size: Int: the size of the asset file in bytes.width: Int: The width of the image, if the asset has a mime type ofimage/*and has a pixel size.height: Int: The height of the image, if the asset has a mime type ofimage/*and has a pixel size.
Resulting in a GraphQL type of:
Image Transformation: The url field on Asset accepts a transform argument which is an input type allowing parameters like width, height, format, quality, focus area, etc. For example:
This would return a URL (on our CDN) that delivers the image resized to 800x600 in WebP format with 90% quality. The transformations are handled by the CDN on request. This is extremely useful to get thumbnails or specific sizes without storing multiple versions.
If the asset is not an image, the transform argument is just ignored, and you get the original file URL.
The supported transform options include:
width, height (pixels)
quality (0-100)
format (PNG, JPG, WEBP)
resizeStrategy or fit (e.g., scale)
focus (for focal point)
Please refer to the Asset API for details.
User Data Stores
In some cases, you may want to store additional user-specific data that’s related to content. Think of users wanting to bookmark content, store content customizations, or store the reading state. Content Cloud provides rich user data stores for this that can be used in all content delivery APIs. Data is stored per space, so it is shared between environments but unique to each user. User data can be stored, retrieved, and used in filters. E.g., when a user opens an article, you can assign a reading timestamp; later, when you want to show the content the user hasn’t read yet, you can filter for content without a reading timestamp.
After creating a user data store in the Content Sync backend, you can opt-in to receiving the user data associated with an entry. The user data is specific to the user requesting the content and only works when providing a user ID in the JWT.
Please note that requests leveraging user data stores may only go to the live and preview endpoints. Trying to load or update user data using the CDN endpoint will result in an error.
The user ID is taken from the sub_id JWT property. If no sub_id is found, the sub property is used instead. If you are using a third-party authorization service like Auth0, AWS Cognito, or Microsoft Entra, this will be the user ID in that system. If you control the user ID yourself through self-signed JWTs, we recommend assigning a namespaced user ID and avoiding the use of personal information like email addresses (emails may also change). Ideally, use a unique ID that’s namespaced to the source system, e.g., { "sub": "{SOURCE_SYSTEM_PREFIX}/user/{USER_ID}", ... }.
Retrieving user data
To fetch content and include user data with it:
This will allow you to query an entry and, if the user has stored user data with it, the associated user data:
This would return the bookmarked and readAt properties of an example FlagsUserData type.
Adding and updating user data
To add or update user data use the corresponding user data mutation:
Permissions
Retrieving user data requires the permission:user-data:read permission. Writing user data requires the permission:user-data:read and permission:user-data:write permissions.
Users can only retrieve data from the user data stores that are explicitly allowed through their JWT. E.g., to provide access to the ExampleContentUserData store, a user must have either the content-user-data:ExampleContentUserData scope or the allow-all scope content-user-data:*.
Limitations
User data stores don’t support nesting, so properties have to use flat types like strings, numbers, or JSON data.
Advanced
Shared IDs (group data)
For advanced use cases, you can also create a shared user ID, e.g., for all users in a specific group. JWTs with shared IDs should be as limited in scope and use as possible. JWTs with a shared ID allow you to synchronize settings across multiple users, e.g., to store progress within a project. You can control which user can read or write to the user data store by assigning the user data read and/or write permissions.
Settings
To store, e.g., user-specific settings that are independent of content, create a user data store for those settings, then create a static content item with a specific customId or UUID and use this content item to store user-specific settings that can be shared across devices. To store settings for different front-ends, you can either create different user data stores or use different content items for different settings; it is generally more efficient to use more content and less types.
Errors
No user data available
When the environment doesn’t have any user data store or the user doesn’t have access to them, Content Cloud will respond with a 400 Bad Request status code and an error message like "\"ExampleForMissingType\" is invalid for user_data_types: This environment doesn't provide user data.".
The requested user data type is not available
When the environment doesn’t have the requested user data store or the user doesn’t have access to it, Content Cloud will respond with a 400 Bad Request status code and an error message like "\"ExampleForMissingType\" is invalid for user_data_types: Invalid user data type ExampleForMissingType.".
No user provided
When no user ID is provided, Content Cloud will respond with a 400 Bad Request status code and an error like "\"ExampleForMissingType\" is invalid for user_data_types: User ID is required to access user data.".
Collection Fields (Pagination in GraphQL)
As noted, for each content type with relational fields or for root query collections, there are Collection types. These types contain pagination info and the list of items.
Arguments on Collection Fields: When you query a collection (either via root field like articleCollection or a subfield like friendsCollection on an entry), you can usually provide:
limit: Int: how many items to return (max 1000) in that page.skip: Int: how many items to skip (offset).where: <TypeName>Filter: a filter object to filter results (see Filters below).order: [<TypeName>Order]: an array of sort keys to order the results.locale: locale in which to retrieve these entries (if not request default).
For example:
This would fetch 5 articles (after skipping 10) whose title contains "GraphQL", sorted by title ascending.
On nested collection fields (like friendsCollection inside an Author), you can similarly do:
to get first 10 friends sorted by name.
If you omit limit, a default of 100 is used (for root collections). If you omit skip, default is 0 (start from beginning).
Return Value (Collection Type): The collection type includes:
items: the array of results (could be empty if none).total: total number of items that match the query (ignoring pagination). This is useful to know if there are more pages.limit: the limit value used for that query (may be null if default used, but often it echoes what you passed).skip: the offset used (or 0).
Use total to implement pagination UI (like showing "Page 1 of X").
Max page size: You cannot retrieve more than 1000 items in one page (the API will cap it even if you request a higher limit). For extremely large sets, you’d use skip/limit to page through.
Collection Filters (Where Clause)
GraphQL provides a powerful filtering mechanism via the where argument on collection queries. The where argument accepts an input object (named <TypeName>Filter) that allows specifying criteria on fields, linked fields, and metadata.
For example, to filter articles by author name and publication date:
This would return articles whose author’s name is exactly "Jane Doe" and whose publishDate is after Jan 1, 2023.
Filter Generation: The filter input type is auto-generated from your content type’s fields:
For each field, filter fields are created based on the field type:
Text/String fields: filters include:
<field>_exists(Boolean): true if field is present (not null), false if not. An empty string counts as “exists”.<field>: exact match, case-sensitive for Symbol field. Only available for short text.<field>_not: not equal to a value. Only available for short text.<field>_contains: substring search (case-insensitive). Only available for short text.<field>_not_contains: substring search (case-insensitive). Only available for short text.<field>_matches: For full-text search across all text fields, they have a separate mechanism (a global query filter in REST, but in GraphQL you’d typically use_containson specific fields, or do a full-text in each field).Example:
title_contains: "GraphQL"finds titles that contain"GraphQL"as a substring (case-insensitive).
Number fields: filters include:
<field>(equals),<field>_gt(“>”, greater than),<field>_gte(“>=”, greater than or equal to),<field>_lt(“<“, less than),<field>_lte(“<=”, ,less than or equal to),<field>_exists: true if the field is present (not null), false if not. 0 counts as “exists”.<field>_not(not equal).Example:
quantity_gte: 100to find entries with at least 100 as their quantity.
Boolean fields: filters include:
<field>(expect true/false),<field>_exists(true if set to either true/false, false if null).Typically not
<field>_notas you’d just use <field>: false to filter false, but you can also negate the expression with this filter.
Date/DateTime fields: similar to numbers:
<field>_gt, _gte, _lt, _ltefor chronological comparisons, identical to numbers.They expect ISO 8601 strings as input.
Example:
publishDate_lte: "2023-12-31T23:59:59Z"for entries published on or before end of 2023.
Location fields: You might get:
<field>_near: filter by proximity to a given coordinate within a radius.For example:
location_near: { latitude: 48.8566, longitude: 2.3522, distance: 100 }would fetch entries within 100 km of Paris.
<field>_within: provide a bounding box coordinates.
Reference fields (Links): For single links, if the link is restricted to one type, you can filter by sub-fields of that linked entry.
The filter input will have a sub-object for the link field. E.g., in ArticleFilter, if author is a link to Author, you’ll have:
author: AuthorFilter author_exists: BooleanThis allows queries like
author: { name: "Jane Doe" }as in example, orauthor: { company: { name: "Acme Inc" } }to go deeper if Author links further.
If the link can be multiple types (union), the filter may not allow sub-field filtering (because it’s ambiguous). In such cases, the only filter is by the linked entry’s
sys:e.g.,
linkedItem_existsor maybelinkedItem: { sys: { id: "..." } }if union.
Another approach is to use the reverse
linkedFromquery instead of filtering forward by a union.You can always filter by a reference’s presence: e.g.,
author_exists: trueto get entries that have an author set.Filtering by reference ID directly is supported via
author: { sys: { id: "<ID>" } }.
Array fields (Lists): For array of simple types (e.g., list of keywords):
<field>_contains_some: [...](matches if at least one element is in the list),<field>_contains_all: [...](matches if the entry’s list contains all the specified values),<field>_contains_none: [...](no element in the entry’s list is in the given list).For arrays of references or links, similar logic:
e.g.,
tags_contains_all: ["tag1","tag2"]to ensure the entry has all those tag IDs.Or
tags_contains_some: ["tag1","tag2"]for at least one.Or
tags_existsfor any tags at all.Example:
recipeCollection(where: { ingredients_contains_all: ["flour","sugar"] }) { ... }would find recipes whose ingredients list includes both"flour"and"sugar".Meanwhile:
recipeCollection(where: { ingredients_contains_some: ["nuts","gluten"] }) { ... }might find recipes that include either "nuts" or "gluten" in ingredients.And:
recipeCollection(where: { ingredients_contains_none: ["peanut"] })to get recipes that have no "peanut" in ingredients (for allergy-safe filtering).
Logical Connectives: By default, multiple conditions in the where object are ANDed together (all must match). To do OR or more complex logic, the filter input has special keys
ANDandORwhich take lists of filter objects:{ OR: [ { condition1 }, { condition2 } ] }means either condition1 OR condition2 matches.{ AND: [ { cond1 }, { cond2 } ] }explicitly AND (though just listing them has same effect).Example:
articleCollection(where: { OR: [ { title_contains: "GraphQL" }, { body_contains: "GraphQL" } ] }) { ... }This finds articles where either the title or body contains "GraphQL" (or both).You can combine these to build complex queries (note the complexity still counts all possibilities though).
Sys Filters: The filter object also allows filtering by system fields under a sys property. For example:
articleCollection(where: { sys: { id: "12345" } }) { ... }would fetch article with that ID (though you’d typically just usearticle(id:"12345")for that). More useful might be:sys: { publishedAt_gt: "2023-01-01T00:00:00Z" }to find content published after a date.
sys.metadata (Tags) Filters: To filter by tags, you use the
sys.metadatain the filter. For example:articleCollection(where: { sys: { metadata: { tags: { id_contains_all: ["abc123"] } } } }) { ... }would find articles that have the tag with ID"abcdef".User data filters: You can filter by user data using a special filter property
sys.*UserData.*that is automatically generated for each available user data store that the current user has access to. For example:articleCollection(where: { sys: { flagsUserData: { bookmarked: true } } }) { ... }: Only return content the user has bookmarked.articleCollection(where: { sys: { flagsUserData: { readAt_exists: false } } }) { ... }: Only return content the user has not read yet, so content that is new to the user.
Limitations: Not all combinations of filters are be allowed. For example, filtering within a union reference is not possible, as mentioned. You also cannot filter on fields of linked collections directly. For instance, “find Authors who have at least 5 books” is not directly possible in one query (no filter like booksCollection_total_gt: 5). You’d handle that logic client-side or via separate query.
Collection Order (Sorting)
When retrieving collections, you can sort the results using the order argument. In GraphQL, order is often an array of values, where each value is a predefined enum combining a field and direction.
For each content type, the schema generates an <TypeName>Order enum. This enum has values like title_ASC, title_DESC, publishDate_ASC, publishDate_DESC, etc., for each sortable field. You can specify one or multiple order criteria.
Examples:
To sort authors by name ascending:
order: [name_ASC]To sort blog posts by publishDate descending:
order: [publishDate_DESC]To sort by multiple fields: e.g., first by category (asc), then by title (asc):
order: [category_ASC, title_ASC].
Multiple ordering works like SQL’s “ORDER BY …, …”: it sorts by the first field, and when there are ties, by the second, etc.
Supported fields for order: Generally, you can order by:
Scalar fields (Text, Number, Date, Boolean). Note: Booleans sort as false < true.
You cannot order by Rich Text, JSON, or Location fields.
You cannot directly order by reference fields (you’d order by a subfield via filter perhaps, but order won’t follow a link).
The sys fields often have
sys_id_ASCandsys_id_DESCas options. GraphQL includessys_id_ASCandsys_id_DESCin theOrderenum for each type, which correspond to the entry’s ID ordering, as well as maybesys_firstPublishedAt_ASCetc.
Default order: If you do not specify an order, the default ordering is by sys.updatedAt descending (i.e., recently published first) and then sys.id ascending to break ties. This means by default you get newest entries first. For nested collections, it defaults to the order they are linked in the entry (i.e., the order stored in the array). For queries using search, the default order is to sort by relevance.
If you want a stable specific order, always set it explicitly.
Reverse order: Instead of an explicit enum, just use the DESC variant. For example, to reverse chronological: order: [sys_firstPublishedAt_DESC].
Ordering on linked fields: GraphQL does not allow something like order: author.name_ASC directly. Instead, if you need to sort by a property of a linked entry, you might have to do that client-side after fetching, or restructure your query: For instance, to get authors sorted by the number of blog posts they have, you can’t directly do that in one query. You’d retrieve authors and maybe sort client-side or do multiple queries.
Limitations: Not all fields may be allowed in order. Particularly, large text fields are not allowed and you cannot order by fields inside Rich Text or JSON; short/string fields, numbers, dates are fine to order by.
If you attempt to order by something unsupported, the query will likely error during validation (unknown enum value).
Default Ordering
As mentioned, the default for top-level collections is newest first, so in Content Cloud GraphQL, if you do:
you’ll get the latest published articles first. If two articles have the exact same publishedAt, the one with the smaller ID comes first.
For nested collections (like an entry’s multi-reference field), the default respects the order as stored in the entry (the order the author arranged those references). So if you don’t specify an order for a field like friendsCollection, you get the order the content editor set in the source site/system, e.g., Drupal.
Single Resource Fields
As mentioned, the root Query includes fields to fetch a single entry or asset by ID for each type. For example, Query.asset(id: "...") or Query.product(id: "..."). These are handy when you know the ID.
Arguments: The single entry fields typically take:
id: String: one ID property for the entry/asset required and only one may be provided.customId: String: one ID property for the entry/asset required and only one may be provided.uuid: String: one ID property for the entry/asset required and only one may be provided.locale: String(optional) to fetch a specific locale’s version.
They return either the entry object or null if not found (or not allowed).
Example usage:
This gets the product with that ID in French.
If the entry doesn’t exist (wrong ID or it’s archived/deleted), you get product: null in data (and no error, because it’s a valid query, just no result). So your app should handle null (not found).
Single resource queries are efficient for fetching a specific item and can be easier than filtering if you have the ID.
One minor note: GraphQL doesn’t have a built-in error code for “not found” – it just returns null for the field. If you ask for a non-existent ID, you just get null data.
For assets, single asset query is similarly asset(id: "...") and returns an Asset or null.
Ordering Nested Collections
By default, nested collection fields (multi-reference fields on entries) return items in the order they were stored (the order that was set in the source site/system, e.g., in the Drupal UI). If you want to sort a nested collection differently in your output, you can supply an order argument to that collection field.
For example:
This will give the author’s books sorted by publish date newest first, rather than the order the books are listed on the author entry.
If you omit the order, as stated, the API uses the insertion order for that list.
One limitation: ordering a nested collection does not change the stored order in the database; it’s just for that query’s output.
If a nested collection has many items and you want to order and page it, you can combine order with limit/skip on that field’s collection.
Exploring the Schema using Apollo
When developing, it’s very helpful to introspect the GraphQL schema and try out queries interactively. Content Cloud provides a dedicated UI in the Content Sync backend to explore the APIs and build queries using auto-complete that you can copy into your application.
Please note that the dev UI completely avoids caching and will be significantly slower than the regular content API.
GraphQL Errors
The GraphQL API follows the standard GraphQL error-handling approach:
If there is an error executing your query (for example, a field doesn’t exist, you used a wrong type in a filter, or the complexity limit was exceeded), the response will still return a 200 OK status, but the JSON will contain an
"errors"array alongside any partial"data". Each error in the"errors"array typically has:message: a human-readable description of the error.locations: where in your query the error occurred (line and column).path: which field or sub-field failed (for runtime errors).extensions: additional info such as error code or error type.
Some common errors:
Validation Errors: These are detected before execution. For example, querying a field that doesn’t exist on a type will yield an error like
"Cannot query field \"foobar\" on type \"Article\"."and the query won’t execute at all. Similarly, if your filter object has an unknown key, you get a validation error.Missing Required Arguments: If you call
blogPostwithout any of the required id filters, you’ll get a validation error complaining an argument is missing.Authentication Errors: If your token is invalid or missing, the API will return an error with message about authorization. This will come as a
401 HTTPresponse instead of a GraphQL error.Rate Limit Exceeded: If you hit the rate limit, the server will abort and return a
429status.Complexity Limit Errors: If your query is too complex, the API will return an error in the GraphQL errors array like
"Query complexity limit exceeded". No partial data will be returned in this case (the query is not executed).Partial Data Errors: If one field in your query errors out (like a resolver for an external resource fails), GraphQL can return partial data for the fields that succeeded, and an
errorsentry for the field that failed. Thepathin the error tells you which field isnulldue to error.Example: If you query an image transform with an invalid option, that field returns
nulland an error about"Invalid asset transform parameter".
Field Resolve Errors: e.g., if you query
someLink { ... }and the linked entry is not found or you don’t have access, GraphQL will returnnullwithout error (treating it as missing).
When debugging, the message is your starting point. For instance:
"Unknown argument \"foo\" on field \"ArticleCollection\"": you used a wrong arg name."Cannot return null for non-nullable field BlogPost.title.": this suggests data integrity issue (the API found a null where it promised not to, which usually means some inconsistency in the content itself)."Too many requests": rate limiting.
In a client application, you should always handle the possibility of errors. Many GraphQL client libraries provide the errors array separately from data.
Security Note: Error messages can sometimes leak field names or reveal if a resource exists. E.g., if you query an entry by ID that doesn’t exist, you might just get null with no error which is good for not exposing info; a well-behaved API won’t say "Entry not found" vs "Entry forbidden" distinctly as that could leak existence.
Error handling: log or inspect the errors array in your responses during development. In production, you might not want to show raw GraphQL error messages to end users as they can be technical.
If you encounter an error you don’t understand, double-check your query against the schema (maybe a typo or misuse), ensure your token has the right permissions, and consider simplifying the query to isolate the issue.