Search documentation

Search documentation

Developers

GraphQL

Query Contoprix through the tenant-aware GraphQL endpoint.

Query published content with GraphQL#

Contoprix exposes a tenant-specific GraphQL delivery endpoint at:

Code
POST /graphql

GraphQL is useful when one screen needs an exact combination of published page, navigation, content, relation, or media fields. REST and @contoprix/client are still the simplest choice for common page and content delivery.

Before you send a query#

Create an API client for the intended website with the graphql:read scope. This is separate from delivery:read; a REST delivery key alone is rejected by /graphql.

The request uses a delivery key in the x-contoprix-delivery-key header, or an SDK bearer access token with the same scope. Keep either credential on the server.

A minimal server-side fetch
const response = await fetch(new URL("/graphql", process.env.CONTOPRIX_BASE_URL!), {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-contoprix-delivery-key": process.env.CONTOPRIX_GRAPHQL_KEY!,
  },
  body: JSON.stringify({
    query: `query Site { site { id name domain } }`,
  }),
  cache: "no-store",
});

Use a separate CONTOPRIX_GRAPHQL_KEY when you want to make the required scope obvious. It can be the same physical API client as other server-side calls only if that client intentionally has both scopes.

Start with stable system fields#

These root fields exist for every tenant:

Root fieldWhat it returns
siteWebsite identity and default language
navigation(locale)Published navigation items
page(path, locale)A published page by path
pageById(id, locale)A published page by ID

Here is a safe first query that works without knowing a custom content model:

Site and navigation
query SiteNavigation($locale: String) {
  site {
    id
    name
    code
    domain
    defaultLanguageCode
  }
  navigation(locale: $locale) {
    id
    name
    url
    openInNewTab
    children {
      id
      name
      url
    }
  }
}
Variables
{ "locale": "en" }

To fetch a published page by path:

Page by path
query PageByPath($path: String!, $locale: String) {
  page(path: $path, locale: $locale) {
    id
    name
    slug
    locale
    blocks {
      id
      kind
      regionCode
      content {
        __typename
      }
      contents {
        __typename
      }
    }
  }
}
Variables
{ "path": "/about", "locale": "en" }

Your content-model fields are generated#

Content-type root names and fields come from the current tenant's content model. A content type with code article becomes an article root. A code such as blog_post becomes blogPost.

For a collection type, the root returns a connection with nodes, totalCount, and pageInfo. This example is valid only when the tenant has an enabled article collection with title and slug fields:

Example article collection
query Articles($locale: String, $after: String) {
  article(first: 10, after: $after, locale: $locale) {
    nodes {
      id
      title
      slug
    }
    totalCount
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Do not copy a sample field name blindly. Inspect the schema for your tenant or generate its types first.

Filter and paginate a collection#

Collection roots accept these common arguments:

ArgumentMeaning
firstPage size from 1 to 100; default is 20
afterCursor from the previous response's endCursor
localeRequested language code
where: { id }Return a specific entry by ID
where: { slug }Return a specific entry by slug
sort: { direction }ASC for oldest first or the default descending direction

Example: request the next page only when the previous response says there is one.

Cursor pagination
let after: string | undefined;

do {
  const data = await graph.request<{
    article: { nodes: unknown[]; pageInfo: { hasNextPage: boolean; endCursor?: string } };
  }>(ARTICLE_QUERY, { after, locale: "en" });

  save(data.article.nodes);
  after = data.article.pageInfo.hasNextPage ? data.article.pageInfo.endCursor : undefined;
} while (after);

Use the GraphQL client package#

The framework-neutral client appends /graphql, adds the correct header, supports timeouts, and distinguishes transport errors from GraphQL response errors.

Terminal
npm install @contoprix/graphql-client
lib/contoprix-graphql.ts
import { createContoprixGraphQLClient } from "@contoprix/graphql-client";

export const graph = createContoprixGraphQLClient({
  endpoint: process.env.CONTOPRIX_BASE_URL!,
  auth: {
    type: "deliveryKey",
    deliveryKey: process.env.CONTOPRIX_GRAPHQL_KEY!,
  },
  locale: "en",
  timeout: 10_000,
});

Use graph.request() when any GraphQL error should throw. Use graph.requestWithErrors() when a UI can safely render partial data alongside the returned GraphQL errors.

Read a stable root
const site = await graph.getSite();
const page = await graph.getPage({ path: "/about", locale: "en" });

Generate types from the live schema#

The public /graphql endpoint disables arbitrary introspection outside development by default. For reliable schema tooling, use the scope-gated schema export through the CLI:

Terminal
npx contoprix graphql pull
npx contoprix graphql generate

# Or run both steps:
npx contoprix graphql sync

These commands need a CLI API client with schema:read. They save the introspection export to .contoprix/schema/graphql-schema.json and generate TypeScript output at the configured graphqlGeneratedTypesFile path, normally src/contoprix/graphql-generated.ts.

Keep GraphQL requests safe#

  • Send only published delivery queries; the endpoint is a read-only delivery surface.
  • Use variables for changing IDs, paths, slugs, and locales.
  • Request only the fields the UI needs.
  • Keep the credential server-side.
  • Handle both HTTP failures and GraphQL errors in the JSON response.
  • Respect cursor pagination instead of asking for large collections.

For a browser interface that helps explore a development schema, see Graph Playground. For standard website delivery, the SDK is the shortest path.