---
title: "GraphQL API"
description: "The public, read-only Primitive GraphQL endpoint, its SDL, rate-limit headers, and the GraphQL versioning and deprecation policy, including the fields deprecated today."
canonical: "https://docs.primitive.dev/docs/graphql"
last-updated: "2026-05-17T00:00:00.000Z"
---

# Primitive GraphQL API

Primitive publishes a public, read-only GraphQL surface at `https://api.primitive.dev/v1/graphql` (also served at `https://api.primitive.dev/graphql`). It needs no credentials and returns synthetic sample data shaped like the REST API, so agents can explore a typed schema and run real queries. The authenticated [REST API](/docs/api) at `https://api.primitive.dev/v1` remains the system of record.


```
curl -s https://api.primitive.dev/v1/graphql \
  -H 'content-type: application/json' \
  -d '{"query":"{ apiVersion emails(first: 2) { edges { node { id subject } } pageInfo { hasNextPage endCursor } } }"}'
```


## Schema

- Introspection is enabled. Send the standard `__schema` query.

- SDL: the authored schema is published as text at [https://api.primitive.dev/graphql.graphql](https://api.primitive.dev/graphql.graphql) (`application/graphql`). It carries every description, the `@deprecated` annotations, the `@cost` and `@rateLimit` directives, and the versioning policy.

- A `GET` on the endpoint with no `query` returns a short HTML page for browsers, or the SDL when the request sends `Accept: application/graphql` or `Accept: text/plain`.

The schema uses the usual conventions: Relay cursor pagination (`Connection` and `PageInfo` types), typed error result unions plus Relay-style `userErrors` on mutation payloads, an async job type you poll with `jobResult`, and a batch mutation (`bulkArchiveEmails`).

## Versioning and deprecation policy

The GraphQL schema is versioned through the `apiVersion` field, which returns `v1` today.

- Within a version, every change is additive and backward-compatible. New types, fields, arguments, and enum values can appear at any time, so clients should ignore fields they do not recognize.

- A field scheduled for removal is marked with GraphQL's standard `@deprecated` directive. The `reason` names the replacement and the date after which the field is removed.

- A deprecated field keeps working until its removal date.

- Breaking changes ship only under a new version, never silently inside `v1`.

### Currently deprecated fields

FieldReplacementRemoved after`Query.job``Query.jobResult`, which returns a typed `JobResult` union so an unknown job id is data rather than `null`2026-12-01`Email.snippet``Email.bodyText`2026-12-01

GraphQL introspection omits deprecated fields unless you ask for them. To see both fields and their reasons, pass `includeDeprecated: true` on each type:


```
{
  query: __type(name: "Query") {
    fields(includeDeprecated: true) {
      name
      isDeprecated
      deprecationReason
    }
  }
  email: __type(name: "Email") {
    fields(includeDeprecated: true) {
      name
      isDeprecated
      deprecationReason
    }
  }
}
```


The REST API has its own policy, based on the `Deprecation` and `Sunset` headers. See [API versioning and deprecation policy](/docs/versioning).

## Limits

- Rate limit: 60 executed queries per minute per client IP. Executed queries, whether sent by `POST` or by `GET` with a `query` parameter, return `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, and `RateLimit-Policy`. If the rate limiter is temporarily unavailable, the query still runs but the response carries none of these headers, so treat their absence as "no budget information" rather than "no limit". Over the limit the endpoint returns `429` with `Retry-After`. Browser clients can read these headers because they are listed in `Access-Control-Expose-Headers`.

- Body size: request bodies are capped at 16 KB. A larger body gets a `413`.

- Mutations must use `POST`. A mutation sent by `GET` gets a `405`.

Maintained by the [primitive.dev team](/about) · [Trust & security](/trust)Last updated: May 17, 2026
