GraphQL API
Alongside the REST v2 API, VeroXM exposes a public, read-only GraphQL API for pulling content, media, and collection metadata into any client. It is code-first (the schema below is generated from typed model classes, not hand-written SDL), lives on its own endpoint, and reuses the exact same content-reading logic as REST v2 - same cache, same field-shaping, same locale-fallback behavior. Nothing about what content comes back differs between the two transports; GraphQL's advantage is letting a client ask for exactly the fields it needs in one round trip, including nested relations, instead of over-fetching a fixed REST shape.
Why GraphQL
- One round trip, exactly the fields you need. A REST response has a fixed shape - you either get more than you asked for or make a second request for a related field. A GraphQL query gets the entry, its related entries, and their media in a single request, shaped exactly like the query that asked for it.
- A typed, introspectable schema. Point any GraphQL client or IDE plugin at the endpoint and it can show you every query, argument, and field VeroXM exposes - no separate reference to keep in sync by hand.
- Pay only for what you use.
totalCounton a list query only runs its count query when your selection set actually asks for it (see below) - a cost REST always pays, every time, whether you need the number or not.
What makes VeroXM's GraphQL API different
Many platforms bolt a GraphQL layer onto an existing REST API as a second implementation, with its own caching, its own field-shaping, and its own permission checks that can quietly drift from what REST does. VeroXM does not do that:
- No consistency gap. The resolvers below are a thin routing layer over the same
PublicContentServiceREST v2 already uses - same cache-aside reads, same locale fallback, same trashed-row handling. Content published through the dashboard shows up identically on either transport at the same time, because there is only one place that decides what "the current version of this entry" means. - One token, one set of rules, both transports. The same project-scoped API token, the same abilities (
read/create/update/delete), and the same per-token rate limiting apply whether you call REST or GraphQL - there is no separate GraphQL-only auth system to provision or audit. - Guarded by default, not as an afterthought. A public-facing GraphQL schema is an easy target for a pathologically deep or wide query. Every request here is checked against a query complexity budget before any resolver runs (see Query complexity limits below) - you do not have to remember to add that protection yourself before going to production.
- Part of one experience platform, not a bolt-on API. The same query resolves the right locale variant and can sit behind the same Content Experiments data your REST integration uses - GraphQL is another way to read the platform's content, not a separate product living beside it.
Endpoint & authentication
POST https://<your-veroxm-host>/public/v2/graphqlAuthenticate with a project-scoped API token (see Authentication), sent as a bearer token - the same token type the REST v2 API uses:
Authorization: Bearer <your-api-token>The token is the request's only source of project identity. Unlike REST v2, there is no project uuid anywhere in the URL or arguments - every query reads from whichever project the token belongs to. A token scoped to the read ability can run every query on this page; a token without it is rejected with 403 Forbidden before any resolver runs. Every request also passes through the same per-token rate limiting REST v2 uses - a 429 response carries a Retry-After header when you are over the limit.
Schema
type Query {
collection(slug: String!): CollectionMeta
collections: [CollectionMeta!]!
entry(collection: String!, id: Int!, timestamps: Boolean = false): Entry
entries(
collection: String!
where: [WhereClauseInput!]
sort: String
locale: String
state: String
offset: Int
limit: Int
timestamps: Boolean = false
): EntryConnection!
media(id: Int!): MediaView
}
type Entry {
id: ID!
locale: String
createdAt: DateTime
updatedAt: DateTime
publishedAt: DateTime
data: JSONObject!
}
type EntryConnection {
items: [Entry!]!
totalCount: Int
}
type CollectionMeta {
slug: String!
name: String!
description: String
fields: [CollectionFieldMeta!]!
}
type CollectionFieldMeta {
name: String!
label: String!
type: String!
}
type MediaView {
id: ID!
fileName: String!
fullUrl: String!
thumbUrl: String
thumbWebpUrl: String
fullWebpUrl: String
caption: String
tags: [String!]!
size: Int
width: Int
height: Int
}
input WhereClauseInput {
field: String!
op: String
value: JSONObject
}data on Entry is a JSON blob shaped exactly like the REST API's per-entry response, rather than per-collection typed fields - collections are user-defined and can change field shape at any time, so the schema cannot commit to a fixed type per collection.
Queries
| Query | Returns |
|---|---|
collection(slug) | One collection's field list (name, label, type per field). null if the slug does not exist in this project. |
collections | Every collection's field list - useful for building a dynamic renderer without hardcoding collection shapes. |
entry(collection, id, timestamps?) | A single entry by id. Only ever returns a published entry - there is no state argument on this query, unlike entries. |
entries(collection, ...) | A page of entries, matching REST v2's list parameters field for field: where, sort, locale, state, offset, limit, timestamps. |
media(id) | A single media item by id, including its already-generated thumbnail and WebP variants. |
where takes an array of { field, op, value } clauses, every clause AND'ed together. op matches the REST operator vocabulary - like, not, in, not_in, lt, lte, gt, gte, between, not_between, null, not_null. Omit op entirely for a plain equality match. OR semantics (REST's {or: {...}} form) have no typed GraphQL equivalent yet - use the REST v2 API for that query if you need them.
totalCount is lazy
EntryConnection.totalCount only triggers a count query server-side if your selection set actually asks for it:
query {
entries(collection: "posts", limit: 10) {
items { id data }
totalCount # costs one extra round trip - omit it if you don't need a total
}
}A query that never selects totalCount never pays for that extra lookup - the concrete payoff of asking for only what you need, compared to a REST response that always computes it.
Example queries
query RecentPosts {
entries(
collection: "posts"
where: [{ field: "status", value: "featured" }]
sort: "created_at:desc"
limit: 10
) {
items {
id
data
publishedAt
}
totalCount
}
}query OnePost {
entry(collection: "posts", id: 42, timestamps: true) {
id
data
createdAt
updatedAt
}
}query PostsSchema {
collection(slug: "posts") {
name
description
fields { name label type }
}
}query CoverImage {
media(id: 17) {
fullUrl
fullWebpUrl
width
height
caption
}
}Query complexity limits
Because relation fields inside data can nest, and this is a public-facing API, every request is checked against a complexity budget before any resolver runs. Every field defaults to a complexity of 1, so a query's complexity is effectively its total field count - deep or unusually wide queries are rejected outright rather than allowed to run an expensive resolver chain:
{
"errors": [
{
"message": "Query is too complex: 1240. Maximum allowed complexity: 1000.",
"extensions": { "code": "QUERY_TOO_COMPLEX" }
}
]
}If you hit this, narrow the query - select fewer fields, paginate with a smaller limit, or split one large query into several smaller ones.
Errors
GraphQL errors follow the standard { errors: [{ message, extensions: { code } }] } shape. The codes you will see from this API:
UNAUTHENTICATED- missing or invalid bearer token.FORBIDDEN- the token is valid but lacks the ability the query requires (e.g. a write-only token calling a read query).QUERY_TOO_COMPLEX- see above.- Standard GraphQL validation errors (unknown field, wrong argument type, etc.) for a malformed query, caught before any resolver runs.
GraphQL vs. REST v2
Same underlying content, same auth model, same rate limits - pick whichever fits the client:
- GraphQL - one round trip for exactly the fields you need, typed
wherefilters, a schema you can introspect. Best for a client that wants to shape its own queries. - REST v2 - simpler for a one-off
curl/webhook-style integration, or a client that does not want a GraphQL dependency at all.
No consistency gap between transports