Most of Orbit's machine-readable surface is request-per-resource: the REST API, the OpenAPI contract, the AsyncAPI event catalog, the Postman collection, and the generated SDKs. That shape is fine for a single list or a single record, but it costs one round-trip per resource when you want a contact together with its conversations and messages. Devotel Orbit's GraphQL Explorer closes that gap with a shaped-read surface: one request returns exactly the fields you asked for, nested to exactly the depth you need. This guide covers where the endpoint lives, how auth and scoping work, what the schema exposes, and a catalog of query patterns you can paste into the Explorer and adapt.
Where the endpoint lives, and who can use it
The interactive explorer sits in the dashboard at Developer → GraphQL Explorer (/developer/graphql). It is not a sandbox against our tenant — it runs against your own workspace, under your own session. Access requires one of the developer roles (owner, admin, or developer), the same guard the rest of the Developer section uses, so you run it signed in rather than with a separate explorer-only credential.
Two HTTP endpoints back the page:
POST /api/v1/developer/graphql— executes a query. The body is{ "query": "...", "variables": {...} }and the response is the standard GraphQL envelope:dataon success,errorswith a readable message plus a path when something in the query was rejected.GET /api/v1/developer/graphql/schema— returns the schema rendered as GraphQL SDL, served verbatim next to the OpenAPI and AsyncAPI contracts. The SDL is generated from the same schema descriptor the executor validates against, so the documented schema cannot drift from the executed one.
The same developer-role guard and the same tenant-scoped auth apply to both. There is no public or unauthenticated GraphQL surface, and the explorer page itself is only visible to developer roles.
Read-only, tenant-scoped, and nothing outbound
Two properties matter when you hand this surface to a teammate or a scheduled report job. First, the surface is read-only: the schema defines a query root and no mutations at all, so a malformed or over-creative query cannot create, modify, or delete anything. Second, every query resolves against the caller's own tenant schema only — the same tenant isolation the rest of the developer routes enforce — so a query never reaches another workspace's data, and the rows it returns are rows from your account.
The schema in one diagram
The schema covers the four highest-traffic CPaaS/CDP resource types, and the edges between them:
- Contact — a CDP contact profile. Queryable directly, and it carries nested edges to its
conversationsandmessages. - Conversation — a multi-channel thread with channel, status, an unread count, and nested
messages. - Message — a single inbound or outbound message with channel, direction, from/to, body, status, and delivery timestamps.
- Segment — an audience definition with a member count and an auto-refresh flag.
The query root exposes three listable collections plus single-record lookups:
query {
contact(id: "...") { ... } # one contact by id
contacts(limit, offset, lifecycleStage) { ... } # list, newest first
conversation(id: "...") { ... } # one conversation by id
conversations(limit, offset, status) { ... } # list, most recently active first
segments(limit, offset) { ... } # audience definitions
}List arguments paginate the same way on every root collection: limit accepts 1–100 with a default of 25, and offset skips rows from the start. Nested edges cap smaller by design — a contact's conversations defaults to 10 with a maximum of 50, and messages default to 20 with a maximum of 100 — so an overly broad nested read cannot pull a thousand message rows when you only asked for a handful.
Twelve query patterns, ready to paste
Each pattern below names the shape it solves. In the Explorer, open Developer → GraphQL Explorer, leave variables empty unless stated, and run.
1. The latest contacts, chosen fields only. The fastest smoke test — returns id, name, and lifecycle stage for the five newest contacts.
{
contacts(limit: 5) {
id
displayName
lifecycleStage
}
}2. Contacts filtered by lifecycle stage. Pass a lifecycleStage string to the list — for example customer or lead. The filter is exact, case-sensitive.
{
contacts(limit: 25, lifecycleStage: "customer") {
id
displayName
email
tags
}
}3. Page through a large contact list. Combine limit and offset; page 4 of a 100-per-page list skips 300 rows.
{
contacts(limit: 100, offset: 300) {
id
displayName
createdAt
}
}4. One contact, deeply fetched. The headline nested read — one round-trip for a contact with its recent conversations and the recent messages inside each one.
query ContactOverview($id: ID!) {
contact(id: $id) {
id
displayName
email
conversations(limit: 5) {
id
channel
status
messages(limit: 10) {
id
direction
body
createdAt
}
}
}
}Run it with a variables block — the Explorer has a variables pane, and the SDK shape expects the same JSON:
{ "id": "REPLACE_WITH_A_CONTACT_ID" }5. A contact plus every message edge, flattened. Skip the conversation hop when you only need messages.
query ContactMessages($id: ID!) {
contact(id: $id) {
id
displayName
messages(limit: 20) {
id
channel
direction
status
deliveredAt
}
}
}6. Open conversations, newest activity first. Filter by status — open, closed, archived — and ask only for what a triage view needs.
{
conversations(limit: 25, status: "open") {
id
channel
contactName
unreadCount
lastMessage
lastMessageAt
}
}7. One conversation, full message history paged in.
query ConversationWithMessages($id: ID!) {
conversation(id: $id) {
id
channel
status
contactId
messages(limit: 20) {
id
direction
from
to
body
sentAt
deliveredAt
}
}
}8. Segments and their member counts. Useful for validating that an audience refresh worked, without opening the Segments UI.
{
segments(limit: 100) {
id
name
contactCount
autoRefresh
updatedAt
}
}9. Ask for fewer fields than you think you need. The executor validates against the schema and returns only named fields, so a narrow query is also a cheaper query. Compare:
{
contacts(limit: 25) { id }
}10. Alias a result when the same query serves two audiences. Standard GraphQL aliasing works — a quick way to keep a single query string stable across two callers with different field naming.
{
recentCustomers: contacts(limit: 5, lifecycleStage: "customer") {
contactId: id
name: displayName
}
}11. Validate against the SDL, not against memory. Before hardening a query into a script or a dashboard embed, fetch the live SDL and confirm the field names, argument types, and list-ranges you plan to rely on. The SDL endpoint is the source the executor enforces, so a discrepancy means the SDL changed, not the executor.
12. Read a validation error, then fix the query. A rejected field or argument comes back as a GraphQL error with a message and a path, not a silent 200. When you see an error entry, the data for valid siblings may still be present — keep the good rows and correct the field that failed rather than discarding the whole response.
Using it from code, not just the Explorer
The Explorer is a browser client over the same endpoint your code would call. From a script or backend worker, POST the query and variables to POST /api/v1/developer/graphql with a developer-role credential, parse the data envelope, and treat errors as a per-field signal rather than a transport failure. Query sizes are bounded (the endpoint rejects bodies over 20 KB), and idle requests time out at the client, so long-running exports belong in the existing export surfaces rather than in a single giant GraphQL read.
When GraphQL is the right surface, and when it is not
GraphQL is the right surface when a read spans related resources — a contact plus conversations plus messages — or when the caller genuinely benefits from choosing fields to reduce payload. It is the wrong surface for bulk export, for writes (the schema has none), for analytics aggregation, or for anything that needs to run as an unauthenticated public client. For those, the REST API, the exports, and the event streams remain the correct wires.
Frequently asked questions
Can I write or mutate through this GraphQL surface? No. The schema defines a query root only — there are no mutations. Writes go through the REST API and the dashboard.
What role do I need to run a query? A developer role: owner, admin, or developer. The same role guard that governs the rest of the Developer section applies here.
Is there a rate limit or a query-size cap? Query bodies are capped at 20 KB and per-list limits are bounded (100 at the root, lower on nested edges). Deeply recursive queries are not supported — the schema is shallow by design.
Does a GraphQL query cost anything extra? No. It is a developer surface, not a billed channel — queries are not metered the way outbound messages or voice minutes are.
Can I introspect the schema programmatically? Yes — GET /api/v1/developer/graphql/schema returns the full SDL, which is also what the Explorer renders in its schema pane.
What happens if I query a field that does not exist? The executor rejects it with a GraphQL error naming the field and the path. Valid fields in the same query still return their data.
The takeaway
Orbit's GraphQL Explorer is the shaped-read counterpart to the REST and event surfaces: same auth, same tenant isolation, no mutation risk, and one request where a shaped read used to take three. Open Developer → GraphQL Explorer, run the first paste-ready pattern above, and let the SDL pane tell you exactly what the schema will accept — the schema and the executor share one source of truth, so what you read is what you can run.