Skip to main content

The developer GraphQL shaped-read model

REST endpoints on Orbit return one resource per response. When a read is shaped like a tree — a contact with its conversations, and each conversation’s messages — the per-resource pattern becomes a chain of requests: N+1 round-trips and a client-side join. The developer GraphQL surface is the answer to that shape: you describe the tree, one request returns it nested. This page is the model for that surface: what it is, what it refuses, and which invariants every query rides on. For endpoint-by-endpoint walkthroughs and copy-pasteable queries in seven SDK languages, use the GraphQL querying guide — this page explains the design, that guide shows the calls.

Why a shaped-read surface exists alongside REST

Orbit’s machine-readable surface already speaks four contracts for the per-resource world: the REST API and its OpenAPI document, the AsyncAPI contract for events, and the Postman collection that regenerates from OpenAPI. Those contracts answer “give me resource X” — precisely, one endpoint at a time. A shaped read is the case where none of them fit: the response you want is a contact joined to two other collections, and building it from per-resource endpoints means three sequential requests plus glue code that re-implements a database join in your application. The GraphQL surface covers exactly one such tree — the four highest-traffic CPaaS/CDP resources, contacts, conversations, messages, and segments — so resolution-shaped reads become one request. Everything else, and every write, still belongs to REST: mutations are not part of the surface at all, which is what keeps it small enough to hold invariants over.

A deliberately minimal grammar

The surface accepts a strict subset of the GraphQL language. Supporting the subset — rather than shipping a general GraphQL server — is the point: every rejected feature below is a behavior class the surface can then promise never to have. Supported:
  • One query operation per document, named or anonymous shorthand
  • Variable definitions with $var references and default values
  • Field arguments over scalars, lists, objects, enums, and null
  • Nested selection sets, field aliases, and the __typename meta-field
Not supported, by design:
  • Mutations and subscriptions — the surface is read-only; writes go through the REST endpoints
  • Fragments (fragment, ...spread) — the parser rejects the keyword outright
  • Directives (@include, @skip, custom) — not part of the grammar
  • __schema / __type introspection — only __typename exists; the schema travels as a served SDL document instead

The served SDL is the executed schema

Introspection is absent because the schema arrives out of band: GET /api/v1/developer/graphql/schema returns the full SDL document as plain text, served alongside the OpenAPI, AsyncAPI, and Postman contracts. The SDL and the executor render from the same source. One schema descriptor drives both paths: execution validates every field, argument, and selection against it, and the SDL endpoint prints it. Documentation can never drift from behavior, because there is only one descriptor. That makes pin to the SDL the integration contract: a field listed in the served SDL with arguments it declares will execute; anything not listed fails validation instead of silently returning null. Updates to the surface land in both places at once, and the served document is the diff you integrate against.

Complexity accounting

Depth limits alone do not bound what a query will cost. A selection only three levels deep — 100 contacts, each with 50 conversations, each with 100 messages — fans out to thousands of per-parent resolver calls, and aliases let a query repeat that subtree arbitrarily many times. Left unbounded, that is a cross-tenant denial-of-service vector. So every query pays for what it asks before it runs. The engine statically estimates resolver-backed work: each object-typed field costs one unit per parent that resolves it, a list field multiplies everything beneath it by its page size (limit, clamped to the field’s maximum), and scalar leaves cost nothing because they read from an already-fetched row. Aliased repeats count fully. When the estimate exceeds 1000 units the request is rejected in validation with Query is too complex — before any resolver executes. Nesting depth separately caps at eight levels.

The result envelope, and where it stops

Query-level failures return HTTP 200 with a { data, errors } body: each failing field comes back null at its path with a corresponding entry in errors, so partial results survive one broken subtree. Validation verdicts — an unknown field, a missing sub-selection, the complexity rejection, a parse failure on an otherwise well-formed request — land in the same envelope. That convention stops at the query boundary. Transport failures still answer as ordinary HTTP errors: authentication and role checks reject with 401/403, malformed bodies with 422, and rate limiting with 429. Result-envelope errors never mask those, and you should treat both channels in client code: HTTP status first, then errors inside a 200.

Safety invariants, inherited from the developer routes

The surface holds the same guarantees as every other developer route, because it sits behind the same guards:
  • Authentication and role — an API key or dashboard session with an owner, admin, or developer role, exactly as the REST developer routes require
  • Tenant isolation — resolvers close over one tenant-schema reference derived from the caller’s context and validated as a safe identifier; every statement is parameterized. A query can only ever read the calling organization’s own data
  • Read-only — no write of any kind, and the surface touches no outbound sending path at all: it reads contacts, conversations, messages, and segments

This page vs the guide

Concepts owns the model; the guide owns the mechanics. Read this page to understand what the surface is and what it promises — then do the work in the GraphQL querying guide: the endpoint contract, per-language request examples across the SDKs, and worked queries. For per-resource operations and all writes, the API reference remains the contract; the SDL endpoint above is the GraphQL schema’s public form.