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/graphqlSend 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:
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 });1011 const { data, errors } = await response.json();12 if (errors?.length) throw new Error(errors[0].message);13 return data;14}1516const { 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 total21 }22 }`,23 { limit: 10 }24);Any GraphQL client works too. With Apollo Client:
1import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';23export 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:
| Operation | Needs |
|---|---|
All queries except apiKeys | content.view |
apiKeys | api_keys.manage |
createContentEntry, updateContentEntry | content.edit, plus content.publish to create as published or change whether an entry is published |
deleteContentEntry | content.delete |
createContentType, updateContentType, deleteContentType | content_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}| Code | When |
|---|---|
UNAUTHENTICATED | No key, or a key that isn't valid. |
FORBIDDEN | The key lacks the permission the operation needs. |
BAD_USER_INPUT | An unknown environment, staging that isn't set up, or an unsupported locale. |
NOT_FOUND | contentEntries 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!): ContentType4 contentTypeById(id: ID!): ContentType5 contentEntries(contentType: String!, status: String, language: String, locale: String, limit: Int, offset: Int): ContentEntriesResponse!6 contentEntry(contentType: String!, slug: String!, language: String, locale: String): ContentEntry7 contentEntryById(id: ID!, locale: String): ContentEntry8 apiKeys: [ApiKey!]!9}1011type 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.