GraphQL API
The authenticated GraphQL endpoint at /gql and its interactive playground, requiring an API key or OAuth bearer token, with queries and mutations auto-generated from the Truestamp resource domains and an introspectable schema.
Overview
Truestamp exposes a GraphQL API at the /gql endpoint. It is an authenticated
surface: every query and mutation you execute requires a credential, either an
API key or an OAuth 2.1 bearer token, sent as an Authorization: Bearer header.
The schema is generated automatically from Truestamp’s resource domains (teams,
items, blockchain, entropy, proofs, beacons, webhooks, the knowledge base, and
more), so the available queries and mutations mirror the same operations offered
elsewhere. The knowledge base, for example, is searchable and readable here
through the kbSearch, kbFetch, and kbLinks queries, the same audience-gated
tools the in-app assistant and the MCP server use.
Every query that wraps an operation rather than a record (the three knowledge-base
queries, generateProof and verifyProof, and the seven utility queries) answers
with the same { result, errors } envelope a mutation has: select result for the
operation’s JSON string and errors { code message vars } for anything that went
wrong with that one field. A failure inside the envelope never nulls the rest of the
document, so several of these fields can be aliased in one request and each reports
its own outcome.
Because GraphQL is introspectable, the best way to learn the surface is to open
the interactive playground at /gql/playground and explore the schema live.
This is the companion surface to the JSON:API. Both speak to the same underlying resources over HTTP; pick GraphQL when you want to shape the exact fields and nested relationships returned in a single request.
The endpoint
GraphQL operations are sent as HTTP POST requests to /gql with a JSON body
containing your query (or mutation) string and optional variables. On the
production app the full URL is https://www.truestamp.com/gql; in local
development it is http://localhost:4000/gql.
A minimal request with curl:
curl -X POST https://www.truestamp.com/gql \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ health { status version } }"}'
The health query is a lightweight status check that returns the service
status, the application version, and build. It is a convenient first call to
confirm your credential and connectivity are working before you explore further.
version and build answer different questions. version is the application
version pinned in mix.exs and does not move between releases; build is the
git commit the running artifact was compiled from, so it identifies the deployed
build exactly. build reads "dev" when no build identity was baked in, which
covers a local instance and any image built before the field existed.
Requests are subject to a per-caller rate limit and a query-complexity ceiling, so deeply nested or overly broad queries may be rejected. Keep individual queries focused on the fields you actually need.
A request body to /gql may be at most 528,384 bytes, which leaves room for a
proof of up to 256 KB (262,144 bytes) passed escaped in the proof argument of
verifyProof, where escaping can double its size. The limit is checked before
the body is parsed. A body over it is refused with 413 Content Too Large and
an error whose extensions carry code: "request_too_large" and max_bytes,
the limit; a declared Content-Length over the limit is refused before any of
the body is read. A non-empty body that is not JSON or form-encoded, such as an
application/graphql document, sent without a Content-Length is refused with
411 Length Required and code: "length_required": send it again with its
length. A JSON POST, the normal case, is never asked for its length.
A rate-limit refusal arrives as 429 Too Many Requests, with a Retry-After header in
whole seconds when the pipeline refused the request. In the response body the error
carries code: "rate_limited" and retry_after_ms, the wait in milliseconds: on a
pipeline refusal under extensions, and on an operation-level refusal under vars
alongside the limit that was reached. retry_after_ms is the field to act on; the
header is only present on pipeline refusals.
Limits are enforced across the whole fleet with eventual consistency: each
server counts what it admits and shares that count with its peers within a
fraction of a second, so a burst of many requests sent at the same instant can
be admitted a few percent past the limit before the servers agree. The
per-caller budget is a token bucket: you may spend the whole budget at once and
then continue at the refill rate, and a refusal’s Retry-After is your own
short wait for the next token. The per-address guard below is a window aligned
to the clock minute, so add a little random jitter to its Retry-After rather
than retrying on the exact second; otherwise every refused client retries
together at the boundary.
Before any credential is examined, each API surface also limits requests per client
address (an IPv4 address or an IPv6 /64) to 1,000 per minute. A flood of missing or bad
credentials is therefore refused with the same 429 before it reaches the database, and
a request can be refused before it is authenticated. Authenticated traffic is charged
against both the per-address and the per-user budget.
Two operations are also limited per call, on every surface and inside an MCP script:
proof generation at 300 per minute per account (3,000 fleet-wide) and proof
verification at 30 per minute per account, with a fleet-wide ceiling of 120 per minute
on verifications that contact the public chains. A GraphQL document that aliases
generateProof or verifyProof many times is charged once per alias; a refused alias
comes back with result: null and a rate_limited entry in its own errors, and the
admitted aliases keep their results.
Authentication
Every GraphQL operation you would write a client against requires authentication (see the playground exception below). Two credential types are accepted, both presented as an HTTP bearer token:
- An API key, sent as
Authorization: Bearer <api_key>. - An OAuth 2.1 access token, sent as
Authorization: Bearer <access_token>.
If no credential is supplied, or the credential is invalid or expired, the
endpoint responds with 401 Unauthorized. The response includes a
WWW-Authenticate: Bearer challenge header and a GraphQL-style errors
envelope so clients get a machine-readable hint about what went wrong.
There is one deliberate exception: loading the playground page itself. A GET
to /gql/playground from a browser is served without authentication so the UI
can render. Everything the UI then does is a POST and needs your credential
like any other client, including the introspection call that populates its Docs
panel and autocomplete. So set your Authorization header in the playground’s
own HTTP-headers pane before running anything.
The exemption is written against the browser’s Accept header as well as the
method and path, so it covers a page load and nothing else: a GET carrying an
operation is refused like any other uncredentialed request.
The path half is matched on a segment boundary, so it covers /gql/playground
and paths beneath it and nothing else. A lookalike that merely starts with the
same characters, such as /gql/playgroundx, is not the playground and gets no
exemption; it is refused with a 401 like any other uncredentialed request.
The playground is read-only
/gql/playground serves queries and introspection. Mutations are refused there
with a read_only_surface error whatever credential you present, because it is
the one GraphQL entry point whose page loads without one. Point a client at
/gql to write; it is the same schema and the same credentials.
Read-only is the only difference. A query you run there is executed AS YOU, with
the same actor and team scoping as the same query against /gql, so me returns
your user and your own items and teams are visible. If me comes back null
while your header is set, the credential reached the server but the actor did
not, and you are looking at a bug rather than at an empty account.
Read and write scopes for OAuth callers
Queries and mutations are both POST requests, so the read/write distinction
is enforced in two places for OAuth callers. Any GraphQL call requires the
read scope (api:read). Mutations additionally require the write scope
(api:write); an OAuth token that lacks it is rejected with an
insufficient_scope error before the mutation runs. API-key callers are not
scope-limited, so this split does not apply to them.
The split follows what an operation actually does, not how it feels. Every
operation that only reads is exposed as a query, including ones that take a
substantial payload: generateProof and verifyProof are queries, as are the
kbSearch / kbFetch / kbLinks tools and the utility operations. A read-only
agent holding just api:read can therefore generate and verify proofs without
being handed the ability to create items or change teams.
The interactive playground
The playground at /gql/playground is an in-browser GraphQL IDE. Open it in a
browser to:
- Browse the full schema, including every query, mutation, type, and field, through built-in introspection and the schema documentation panel.
- Compose and run operations with autocompletion and inline validation.
- Set request headers, including your
Authorization: Bearer <token>header, so operations you run from the playground are authenticated.
Because the schema is fully introspectable, the playground’s documentation explorer is the authoritative, always-current catalog of what you can query and mutate. Rather than memorizing operation names, start there and let autocompletion guide you.
Exploring the schema
The GraphQL schema is generated from Truestamp’s resource domains, so the set of available operations tracks the resources and actions the platform exposes. Rather than enumerating them here, use introspection to discover the surface:
- Open
/gql/playgroundand read the documentation explorer, which lists every query and mutation with its arguments and return types. - Run an introspection query (for example, requesting
__schema { queryType { fields { name } } }) from any authenticated client to fetch the same catalog programmatically.
Queries read data; mutations change it. Field selection lets you request exactly the attributes and nested relationships you need in one round trip, which is the main reason to prefer GraphQL over a fixed-shape REST response.
Two families of queries are worth knowing by name. The me query returns the
authenticated caller’s own user record (the GraphQL twin of the REST surface’s
GET /api/json/users/me), a convenient credential check alongside health. And seven
stateless utility queries, hash, verifyHash, uuidv7Timestamp, ulidTimestamp,
resolveId, jcsCanonicalize, and serverTime, mirror the
POST /api/json/utilities/* routes; see
the utility endpoints reference for their arguments, result
shapes, and the visibility gating on resolveId.
Multi-tenancy
Many resources (such as items) are scoped to a team. As with the other
authenticated API surfaces, you can target a specific team by supplying a
tenant value (a team id) on the request; when omitted, operations resolve
against your default or personal team. Supplying a team you are not a member of
is rejected. This is the same tenant-resolution behavior used across Truestamp’s
authenticated HTTP APIs.