JSON:API HTTP Surface
The authenticated JSON:API REST surface at /api/json, its API-key or OAuth bearer auth, the OpenAPI spec plus Swagger UI and ReDoc discovery pages, resource-oriented endpoints, tenant selection, filtering, and pagination conventions.
Overview
Truestamp exposes a JSON:API-compliant REST surface under the base path /api/json.
It is an authenticated API: every endpoint requires a credential except the machine
readable OpenAPI specification and the two documentation pages that render it. Callers
authenticate with an Authorization: Bearer header carrying either a Truestamp API key
or an OAuth 2.1 access token. The surface is
generated from Truestamp’s data model, so resources such as items, blocks, entropy
observations, teams, and webhook endpoints appear as conventional JSON:API
resource endpoints with standard filtering, sorting, and pagination. Proof
generation and verification ride the same surface as action-style routes. Two hosted
documentation pages, Swagger UI and ReDoc, render the live specification so a developer
can browse every operation and its request and response shapes.
Base path and authentication
All JSON:API traffic is served under /api/json. Individual resources hang off that
prefix, for example /api/json/items for the items collection and
/api/json/items/:id for a single item.
Authentication is mandatory. The exceptions are the discovery surfaces: the OpenAPI
document at /api/json/open_api and the two documentation pages that render it
(/api/json/swaggerui and /api/json/redoc). All three are served without a
credential so tooling and developers can discover the API. Every other route rejects
an anonymous request with a 401 Unauthorized.
Present a credential in the standard bearer form:
Authorization: Bearer <api-key-or-oauth-access-token>
Two credential types are accepted on the same header:
- An API key issued to your account.
- An OAuth 2.1 access token obtained from Truestamp’s authorization server.
For OAuth callers, read operations (GET and HEAD) require the api:read scope and
mutating operations (POST, PATCH, DELETE) require api:write. There is one carve-out:
a small, explicitly enumerated set of POST routes that only read require api:read
too. Ash mounts a generic action as a POST route even when it writes nothing, so the
verb alone would over-demand the write scope. The read-only POST routes are the three
knowledge-base tools (/api/json/kb/search, /api/json/kb/fetch, /api/json/kb/links),
the seven utilities (/api/json/utilities/hash, /verify-hash, /uuidv7-timestamp,
/ulid-timestamp, /resolve-id, /jcs-canonicalize, /server-time), and the two
proof operations
(/api/json/proof/generate, /api/json/proof/verify). Every other POST, PATCH, and
DELETE still requires api:write. API-key callers are not scope-limited in this way.
A request with a blank or malformed bearer token is rejected with a 401 rather than a
server error.
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://www.truestamp.com/api/json/items
Requests are rate limited per authenticated caller, falling back to your network
address when a request carries no credential. Exceeding the budget returns
429 Too Many Requests with a JSON:API error object whose code is "rate_limited"
and whose meta carries retry_after_ms, the wait in milliseconds, and limit, the
budget that was reached. Wait that long and retry.
A refusal from the API pipeline also carries a Retry-After header in whole seconds.
Some limits are enforced deeper, on the operation rather than the request, and those
refusals carry the same body without the header, so read meta.retry_after_ms and treat
the header as a convenience when it is present.
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 refusal from these
carries the usual rate_limited body without a Retry-After header.
Request size limits
A request body to /api/json may be at most 528,384 bytes: twice 262,144 plus 4,096. That leaves room for a proof of up to 256 KB (262,144 bytes) even when it is sent escaped inside a JSON string, which can double its size, plus the rest of the document. The limit is checked before the body is parsed.
- Over the limit:
413. A body over the limit is refused with413 Content Too Largeand a JSON:API error whosecodeisrequest_too_largeand whosemeta.max_bytesis the limit. A declaredContent-Lengthover the limit is refused before any of the body is read. A JSON or form-encoded body sent without one, such as a chunked upload, is read only until it passes the limit. On HTTP/1.x a413also closes the connection. - No length where one is needed:
411. Any other non-empty body sent without aContent-Length, such as a multipart upload, a body of another content type, or a body on aGET, is refused with411 Length Requiredand the codelength_required. Send it again with itsContent-Length. An empty body, as on an ordinaryGET, passes.
A JSON body sent with POST, PATCH or DELETE (application/vnd.api+json or application/json), the normal case for this API, is never asked for its length.
Error bodies
Errors come back as a JSON:API errors array. Each entry carries status, code, a
human-readable detail, and, where the error is about a specific input, a source
pointer naming it.
An error with a bound value renders that value in both forms. detail reads as prose
with the value substituted, and meta carries the bindings separately for a caller
that would rather branch on the number than parse the sentence:
{
"errors": [
{
"status": "400",
"code": "invalid_argument",
"source": { "pointer": "/data/attributes/limit" },
"detail": "must be less than or equal to 12",
"meta": { "max": 12 }
}
]
}
Prefer code and meta for anything programmatic. detail is prose and may be
reworded; the pair is offered so that reading it is never the only option.
Discovery: OpenAPI, Swagger UI, and ReDoc
The API is self-describing. The full OpenAPI specification is generated from the live
data model and served as JSON at /api/json/open_api. Because that document is the
canonical source of every operation, path, parameter, and schema, it is the most
reliable reference for exactly what the API accepts and returns.
Two human-friendly documentation UIs render that same specification:
- Swagger UI at
/api/json/swaggerui: an interactive explorer that lists every endpoint and lets you inspect request and response schemas. - ReDoc at
/api/json/redoc: a clean, readable reference view of the same spec.
Both documentation pages and the raw spec are reachable without authenticating, so you can survey the API before wiring up credentials. Actually calling the resource endpoints still requires a bearer token.
Resource endpoints and conventions
The surface follows JSON:API conventions. Each resource is exposed at a base route, and operations map to HTTP verbs:
GET /api/json/<resource>: list a collection.GET /api/json/<resource>/:id: fetch a single record by id.POST /api/json/<resource>: create a record.PATCH /api/json/<resource>/:id: update a record.DELETE /api/json/<resource>/:id: delete a record.
Not every resource exposes every verb. Many are read-only, and the write verbs appear
only where a matching action is published: for example, the items resource supports
listing, fetching by id, creating, and updating under
/api/json/items, while webhook endpoints additionally support delete. Some resources
also expose action-style endpoints; the proof resource, for instance, accepts
POST /api/json/proof/generate to generate a proof and POST /api/json/proof/verify
to verify one, and the knowledge base accepts POST /api/json/kb/search,
POST /api/json/kb/fetch, and POST /api/json/kb/links to search, read, and traverse
the links between concepts (the same audience-gated knowledge-base reads the in-app
assistant and connected AI agents use). The OpenAPI spec is the authoritative list of
which resources and operations exist.
POST /api/json/proof/generate takes the subject id and an explicit subject type
(there is no auto detection), an optional format of json or cbor, and an
optional witnesses array choosing which witness details ride in the bundle. A
witness is a public record that existed before the submission and that the subject’s
fingerprint commits to, so witnesses open the submitted-after edge of the submission
window. Omit witnesses to carry every one of them, which is what makes a downloaded
bundle stand alone; send [] for a compact bundle that carries none; or name a subset
from block, entropy_stellar, entropy_nist, entropy_bitcoin, and
signing_key_event. An unrecognized name is rejected as invalid_witness, and asking
for a witness the subject never captured simply omits it. See
the proof bundle wire format for what each
witness carries.
Transient refusals are worth telling apart from terminal ones, because only the
transient ones are worth retrying. subject_not_ready and no_external_commitments
are transient: the subject or its block has not reached a verifiable stage yet, and
the next commit gets it there. signing_unavailable is transient too: Truestamp
could not sign the proof just now, so the proof is temporarily unavailable. So are
witness_unavailable, where Truestamp could not find a witness the subject’s
fingerprint commits to (meta.witness names it) and issues no proof rather than one
without that witness, and lookup_failed, where Truestamp could not read a record
the proof is built from. In all three cases nothing about the subject is at fault,
and a later request can succeed.
subject_not_recomputable is terminal: the item’s stored claims or metadata no longer
reproduce the hash committed at submission, so no proof over them could be verified by
anyone, and meta.drifted names which half is at fault. If generation fails its own
internal verification for any other reason, the detail and meta.failed_steps name
the verification steps that failed rather than reporting only that something did.
One route to know early: GET /api/json/users/me (the currentUser operation)
returns the authenticated caller’s own user record. It takes no parameters and is the
canonical first call to confirm that a new API key or OAuth
token works:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://www.truestamp.com/api/json/users/me
A 200 with your own user id in data.id confirms the credential; a 401 means the
credential was missing, expired, or malformed.
Two sibling user routes exist on the same resource: GET /api/json/users
(listUsers) and GET /api/json/users/:id (getUser). Both are visibility-filtered
to the caller: the list returns you plus the members of teams you share, a get by id
resolves only for yourself or a shared-team member, and sensitive fields are
restricted field-by-field regardless of who is asking. There is no way to enumerate
users outside your own teams.
Responses can embed related records. A request may ask for related resources to be
included alongside the primary data (for example, an item can include its team, its
creator, and the block it was committed to), following the JSON:API include
convention.
Tenant selection for multi-tenant resources
Some resources are scoped to a team. For those, the request selects which team it
operates within, either with a tenant request header or a tenant query parameter
carrying the team id:
curl -H "Authorization: Bearer YOUR_API_KEY" \
-H "tenant: <team-id>" \
https://www.truestamp.com/api/json/items
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://www.truestamp.com/api/json/items?tenant=<team-id>"
When no tenant is supplied, the request falls back to your default team preference if
one is set and you still have a write-capable role on it, and otherwise to your personal
team. A tenant you are not a member of is rejected with 403 Forbidden.
Utility endpoints
Seven stateless utility operations live under POST /api/json/utilities: hash,
verify-hash, uuidv7-timestamp, ulid-timestamp, resolve-id,
jcs-canonicalize, and server-time. They compute hashes and canonical JSON, extract
the time embedded in an identifier, classify opaque ids, and read the server’s clock;
they create nothing, and their responses use a result envelope instead of a
JSON:API resource document. Each has a GraphQL
query twin. See the utility endpoints reference for the request
and response shape, OAuth scope requirements, and the visibility gating on
resolve-id.
Filtering, sorting, and pagination
Collection endpoints support the standard JSON:API query conventions for narrowing and ordering results, along with pagination for large result sets.
Pagination is controlled with the page family of query parameters:
page[limit]sets the page size and must be a positive integer.page[offset]skips a number of records and must be a non-negative integer.page[after]andpage[before]carry an opaque cursor.
Collections that page by cursor, the items collection among them, return next and prev
links that already carry the right page[after] or page[before] cursor. Follow those
links rather than constructing cursors yourself. The collections that grow without bound -
items, beacons, blocks, entropy observations, commitments and epochs - return a bounded
page of 25 by default even when you send no page parameters, so a first request never
hands back an entire table.
Each resource defines its own default page size, and every paginated collection has a
maximum: 250 unless the resource declares its own. There is no unbounded page. An
oversized page[limit] is clamped to that maximum rather than rejected, so a large value
returns a full page instead of a 400. A clamped page is still a complete page: it
carries the clamped size in meta.page.limit and, when more records remain, a next link
that continues from where it ended. Paging by next therefore reaches every record no
matter what page[limit] was asked for. Supplying a non-integer, zero, or negative
page[limit], or a non-integer or negative page[offset], is rejected with a 400 Bad Request and a JSON:API error body.
Related surfaces
The JSON:API surface is one of several ways to integrate with Truestamp. A GraphQL endpoint exposes the same underlying data model with the same bearer authentication, and outgoing webhooks push delivery notifications to your own endpoints. Those are covered by their own concepts; this concept is the reference for the REST surface only.
Citations
- JSON:API v1.1 Specification. Defines the resource
object structure,
include, sorting, and thepagepagination conventions this surface follows. - OpenAPI Specification. The format of the
machine-readable spec served at
/api/json/open_apiand rendered by the Swagger UI and ReDoc pages.