Graphdocs

GraphQL

Overview & endpoint

One endpoint for reads and writes, with the same keys.

The GraphQL API serves the same content as REST from a single endpoint, and adds every write: creating, updating, publishing and deleting entries, and managing content types. Ask for exactly the fields you need, and fetch an entry with its content type and author in one request.

Endpoint

POST https://api.graphcms.io/graphql

Send a JSON body with query and, optionally, variables and operationName. Authenticate with the same API keys as REST, in Authorization: Bearer or X-API-Key.

curl https://api.graphcms.io/graphql \
-H "Authorization: Bearer $GRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query":"{ contentTypes { name slug } }"}'
{ "data": { "contentTypes": [{ "name": "Blog posts", "slug": "blog-posts" }] } }

Opening the endpoint in a browser (a GET) serves GraphiQL, an in-browser editor with autocompletion and the schema docs. Requests from it still need a key: add the Authorization header in GraphiQL's headers panel.

From JavaScript

No client library is required; GraphQL is a POST with a JSON body:

lib/graphql.js
1export async function graphql(query, variables = {}) {
2 const response = await fetch('https://api.graphcms.io/graphql', {
3 method: 'POST',
4 headers: {
5 Authorization: `Bearer ${process.env.GRAPH_API_KEY}`,
6 'Content-Type': 'application/json',
7 },
8 body: JSON.stringify({ query, variables }),
9 });
10
11 const { data, errors } = await response.json();
12 if (errors?.length) throw new Error(errors[0].message);
13 return data;
14}
15
16const { contentEntries } = await graphql(
17 `query Posts($limit: Int) {
18 contentEntries(contentType: "blog-posts", status: "published", limit: $limit) {
19 entries { id title slug fields publishedAt }
20 total
21 }
22 }`,
23 { limit: 10 }
24);

Any GraphQL client works too. With Apollo Client:

1import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
2
3export const client = new ApolloClient({
4 link: new HttpLink({
5 uri: 'https://api.graphcms.io/graphql',
6 headers: { authorization: `Bearer ${process.env.GRAPH_API_KEY}` },
7 }),
8 cache: new InMemoryCache(),
9});

Environments

Add ?environment=staging to the endpoint URL, or send X-Environment: staging, and every query and mutation in the request uses staging content:

await fetch('https://api.graphcms.io/graphql?environment=staging', { method: 'POST', headers, body });

See Environments.

Permissions

Every operation checks the key's permissions:

OperationNeeds
All queries except apiKeyscontent.view
apiKeysapi_keys.manage
createContentEntry, updateContentEntrycontent.edit, plus content.publish to create as published or change whether an entry is published
deleteContentEntrycontent.delete
createContentType, updateContentType, deleteContentTypecontent_types.manage

Errors

GraphQL responds 200 even when an operation fails, and reports the problem in errors. Authentication, permission, environment and input problems are GraphQL errors with a code in extensions.code:

{
"errors": [
{
"message": "Forbidden - this API key needs the \"content.publish\" permission",
"path": ["updateContentEntry"],
"extensions": { "code": "FORBIDDEN" }
}
],
"data": null
}
CodeWhen
UNAUTHENTICATEDNo key, or a key that isn't valid.
FORBIDDENThe key lacks the permission the operation needs.
BAD_USER_INPUTAn unknown environment, staging that isn't set up, or an unsupported locale.
NOT_FOUNDcontentEntries was asked for a content type that doesn't exist.

Mutations report business failures differently: they return normally with success: false and a message in error, such as Entry not found or Cannot delete content type with existing entries. Check success on every mutation result. See Mutations.

Schema at a glance

1type Query {
2 contentTypes: [ContentType!]!
3 contentType(slug: String!): ContentType
4 contentTypeById(id: ID!): ContentType
5 contentEntries(contentType: String!, status: String, language: String, locale: String, limit: Int, offset: Int): ContentEntriesResponse!
6 contentEntry(contentType: String!, slug: String!, language: String, locale: String): ContentEntry
7 contentEntryById(id: ID!, locale: String): ContentEntry
8 apiKeys: [ApiKey!]!
9}
10
11type Mutation {
12 createContentEntry(input: CreateContentEntryInput!): ContentEntryResponse!
13 updateContentEntry(id: ID!, input: UpdateContentEntryInput!): ContentEntryResponse!
14 deleteContentEntry(id: ID!): DeleteResponse!
15 createContentType(input: CreateContentTypeInput!): ContentTypeResponse!
16 updateContentType(id: ID!, input: UpdateContentTypeInput!): ContentTypeResponse!
17 deleteContentType(id: ID!): DeleteResponse!
18}

fields on an entry is the JSON scalar: one object keyed by field slug, exactly as REST returns it. Dates are ISO 8601 strings.