Skip to main content

Query the GraphQL surface

The REST API maps one endpoint to one resource: fetch a contact, then fetch its conversations, then fetch each conversation’s messages. The developer GraphQL surface is the shaped-read counterpart — you describe the tree you want, and one request returns a contact with its conversations and messages already nested, or a segment with its contacts pulled in a single round-trip. The surface is read-only and deliberately small. It covers the four highest-traffic CPaaS/CDP resource types — contacts, conversations, messages, and segments — so resolution-shaped reads stop needing client-side joins. For everything else (sending messages, managing channels, mutating anything), use the REST API.

When to use GraphQL vs REST

Auth & roles

The GraphQL endpoints sit behind the same guard as every other developer route: authenticate with an API key (X-API-Key: dv_live_sk_…) or a dashboard session, and the calling key or session must carry an owner, admin, or developer role. See Authentication.

Your first request

The query endpoint is POST /api/v1/developer/graphql, with a { "query", "variables?", "operationName?" } JSON body. This query fetches one contact with its conversations and each conversation’s recent messages in a single round-trip:
The same call with the Node SDK’s underlying fetch:
Always check the errors array. Per GraphQL-over-HTTP convention a query that parses but fails during execution still returns 200 OK with data: null (or partial data) and an errors list — it does not come back as a REST-style error envelope.

Schema introspection

The full schema is published as an SDL document, served alongside the OpenAPI spec, AsyncAPI contract, and Postman collection:
It renders these types — Query with entry points contact, contacts, conversation, conversations, and segments, plus Contact, Conversation, Message, and Segment:
The SDL is rendered from the same descriptor that drives execution, so what you download is what the server enforces. Pin requests against the downloaded SDL — a field that isn’t in the schema is rejected, so no request can silently start returning a renamed shape.
The surface runs on a deliberately minimal query language: a single query operation, variables with defaults, field arguments, nested selection sets, and aliases. Mutations, subscriptions, fragments, directives, and __schema introspection are not supported — the SDL endpoint above is the schema contract.

Variables, operation names, and limits

  • query — the GraphQL document string, capped at 20 KB.
  • variables — a JSON object of values referenced as $name in the query. Variable definitions in the query can declare defaults; a request-supplied value overrides the default.
  • operationName — required when the document contains more than one operation. If it names an operation that isn’t in the document, you get Unknown operation named "…" in errors.
Two static guards bound the work a query can trigger, checked before anything executes:
  • Depth cap — a selection set nested more than 8 levels is rejected.
  • Complexity cap — each resolver-backed field costs one unit, and every list field multiplies everything beneath it by its (bounded) page size; a selection set estimated over 1000 units is rejected with Query is too complex: …. Reduce nesting, lower the limit arguments, or drop repeated aliased subtrees.
Both guards return the standard GraphQL envelope — 200 OK with { "data": null, "errors": [{ "message": "…" }] }. Inspector responses carry Cache-Control: no-store and are never cached.

Prototype in the dashboard Explorer

Before wiring code, open Developer → GraphQL in the dashboard. The Explorer runs queries against your live tenant with your session, renders the schema next to the editor, and shows the exact data/errors envelope your code would receive. Iterate on the query shape there, then paste the finished document into your integration.

Pagination & nested collections

List arguments are clamped server-side; values outside the clamp fall back to the defaults below. Top-level lists page by offset (0 starts at the newest row by creation time; conversations order by last activity). Nested collections take a limit only — page their parent (e.g. further conversations) rather than the nested list itself. Tenant isolation: execution is always scoped to the calling key’s tenant. A GraphQL request can never reach another tenant’s data, and there is no cross-tenant query mode.

Walkthrough A — export a contact with all conversations and messages

One REST join costs three sequential requests (GET /contacts/:id, GET /conversations?contact_id=…, then GET /messages?conversation_id=… per conversation). The GraphQL version is a single request:
The response nests each conversation’s messages under its conversation:
If the contact doesn’t exist, data.contact is null with no errors entry — check for null before unpacking.

Walkthrough B — traverse to a segment’s contacts

Resolve segment ids, then pull contacts on the members’ lifecycle stage — the query asks for the exact shape so the response carries just the fields you consume:
For a deeper traversal anchor the segment to one contact and walk out through that contact’s conversations and message bodies — contact(id:) → conversations → messages — all in the same request, still one round-trip end to end.

Errors & failure modes

GraphQL responses differ from the REST envelope in one important way: a query-level failure is not an HTTP error. Parse failures, validation rejections, and per-field resolver errors all return 200 OK with an errors array next to data (which is null or partially populated on failure). Transport-level failures — auth, body validation, rate limits — still come back as HTTP error codes. Use REST error-handling patterns for the HTTP-level failures, and treat the errors array as the GraphQL-level failure channel.

Rate limits

Both endpoints share the tenant-scoped authenticated write bucket — 60 requests per minute per tenant. Buckets are per tenant, so a heavy integration cannot crowd out your dashboard, but your own scripts share the same window. On 429, read details.retry_after (seconds) and back off instead of retrying on a fixed schedule. Cheap request shapes stay cheap: batch the reads you need into one nested query rather than hammering the endpoint with single-field requests.

Frequently asked questions

Can I write through GraphQL?

No. The surface is read-only — there are no mutations. Sends, updates, and administrative actions all stay on the REST API.

Why did my query return 200 with an errors array?

That’s the GraphQL-over-HTTP convention: the request was well-formed at the HTTP layer, and the failure is inside the query. Read errors[0].message; common causes are a syntax error, an unknown field, or the complexity cap.

How do I list more than 100 conversations?

Page the parent: conversations(limit: 100, offset: …) for the next batch. Nested collections carry at most one page per parent by design — they’re shaped reads, not bulk export. For bulk export use the REST list endpoints with pagination.

See also