Knowledge Base

Browse the concepts behind Truestamp. Follow the links between concepts, or search across everything.

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/playground and 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.