Beacon API
The authenticated programmatic beacon surfaces - JSON:API endpoints under /api/json/beacons (list, latest, by id, by hash), the equivalent GraphQL queries, the four-field beacon object, error and rate-limit behavior including the rate_limited code, and beacon proof generation by block id.
Overview
Truestamp beacons are read-only projections of finalized or committed blocks, exposed as verifiable public randomness and proof-of-life markers. Anyone can browse them without an account on the public beacon page; this concept is the reference for the programmatic surfaces, which all require an authenticated caller. Machine consumers (CLI implementers, monitoring jobs, downstream verifiers) read beacons over four JSON:API endpoints under /api/json/beacons, four equivalent GraphQL queries, the MCP server’s code mode, or a real-time console stream. Every surface returns the same four-field beacon object: id, hash, timestamp, and previous_hash.
Authentication and rate limits
The beacon endpoints sit on Truestamp’s standard authenticated API surfaces, so the credential rules are the same as the rest of the JSON:API and GraphQL:
Authorization: Bearer <api-key-or-oauth-access-token>
- An API key works on both surfaces with no scope restrictions.
- An OAuth 2.1 access token needs the
api:readscope; every beacon operation is a read. A token without that scope is rejected with403 Forbidden. - A missing, blank, or invalid credential is rejected with
401 Unauthorized.
Requests are rate limited per authenticated caller on each surface (by default 120 requests per minute each for JSON:API and GraphQL). Exceeding the budget returns 429 Too Many Requests with a Retry-After header and a machine-readable error body carrying code: "rate_limited" and meta.retry_after_ms, the wait in milliseconds. A polling consumer should treat the roughly once-a-minute block cadence as the natural refresh rate; polling faster than that yields no new data.
The beacon object
A beacon exposes exactly four fields, projected from the underlying block:
| Field | Type | Meaning |
|---|---|---|
id |
UUIDv7 string | The underlying block’s identifier, usable for follow-up lookups and proof generation. |
hash |
64-char lowercase hex | The block hash, a SHA-256 digest. This is the beacon value. |
timestamp |
ISO 8601 UTC | When the underlying block was finalized. |
previous_hash |
64-char lowercase hex | Chain-link to the previous beacon’s hash, so a consumer can walk the chain. |
Only finalized or committed blocks ever surface as beacons. Block internals such as the Merkle root, state, signature, and signing-key material are deliberately not part of the beacon shape, on every surface including MCP and the console stream; a consumer who needs them can fetch the full block from GET /api/json/blocks/:id using the beacon’s id. Field names are snake_case on JSON:API and the console stream, and camelCase (previousHash) on GraphQL.
The three single-beacon endpoints return plain JSON, not a JSON:API resource envelope: the response body is the bare beacon object. The list endpoint is the exception. It is a standard JSON:API index, so it returns a resource document with data, links and meta, and each beacon arrives as a resource object with its id alongside the other three fields under attributes.
JSON:API endpoints
All four routes are GET requests under /api/json/beacons:
| Path | Returns |
|---|---|
/api/json/beacons |
A page of beacons, newest first. Keyset paginated; see below. |
/api/json/beacons/latest |
The current head beacon. |
/api/json/beacons/:id |
One beacon by block id (UUIDv7). |
/api/json/beacons/by-hash/:hash |
One beacon by block hash (64 hex characters; matching is case-insensitive). |
A single beacon response looks like:
{
"id": "019db702-b08c-73dc-a7cd-2c5e011f1dad",
"hash": "ffe86dc05a0c7b42279f7fa6afb016cd6928980d24673051fc58731492ce2a1b",
"timestamp": "2026-04-22T21:05:00Z",
"previous_hash": "1c4812bdfec2bf29333136d86bc996f866e38177acc90565a0554c7ec698029b"
}
Lookup notes:
- The
:idlookup validates the id as a UUIDv7 before querying; a malformed id is a400, an unknown id is a404. A block that exists but is not yet finalized or committed is also a404(it is not a beacon yet). - The
by-hashlookup validates the value as a SHA-256 hex string; uppercase input is accepted and downcased before matching. A malformed value is a400, a miss is a404. latestresolves the current head of the chain. Every deployment is initialized with a genesis block, so a head beacon always exists; an empty chain is not a supported response shape.
Paging the beacon list
GET /api/json/beacons pages exactly like /api/json/blocks, /api/json/items and /api/json/entropy_observations: keyset cursors, no offsets. A client that already walks one of those needs no beacon-specific code.
| Parameter | Meaning |
|---|---|
page[limit]=N |
Rows per page. Minimum 1, default 25. A larger value is clamped to the server maximum of 250. |
page[after]=<cursor> |
The opaque cursor carried in the previous response’s links.next. |
page[before]=<cursor> |
The opaque cursor carried in the previous response’s links.prev. |
page[count]=true |
Adds meta.page.total, the number of beacons matching the query. |
sort=id / sort=-id |
Oldest first, or newest first. Newest first is the default when sort is absent. |
A page response carries a links object with first, self, next and prev. On the first page prev is null; on the last page next is null. Follow the absolute URLs verbatim rather than rebuilding them, because the cursor encodes the sort the page was taken under, and a cursor is only meaningful against that same sort.
{
"data": [
{
"type": "beacon",
"id": "019db702-b08c-73dc-a7cd-2c5e011f1dad",
"attributes": {
"hash": "ffe86dc05a0c7b42279f7fa6afb016cd6928980d24673051fc58731492ce2a1b",
"timestamp": "2026-04-22T21:05:00.412330Z",
"previous_hash": "1c4812bdfec2bf29333136d86bc996f866e38177acc90565a0554c7ec698029b"
}
}
],
"links": {
"first": "https://www.truestamp.com/api/json/beacons?page[limit]=1",
"self": "https://www.truestamp.com/api/json/beacons?page[limit]=1",
"next": "https://www.truestamp.com/api/json/beacons?page[after]=g2wAAAABaAJ...&page[limit]=1",
"prev": null
},
"meta": {"page": {"limit": 1}}
}
Because id is a UUIDv7 and blocks are created in UUIDv7 order, sorting on id is the chain’s own chronological order. sort=id therefore starts at the genesis beacon and walks forward.
Two things worth knowing before writing a client:
page[limit]is clamped, never rejected. The OpenAPI document states a minimum of 1 and no maximum, which is why an oversized limit is not an error: asking for more than the server maximum of 250 returns 250 rows rather than a400. The response is still a complete page, someta.page.limitreports the clamped value andlinks.nextcontinues from where it ended whenever more beacons remain. Paging bylinks.nexttherefore reaches every beacon regardless of the limit you asked for, and the same ceiling applies on/blocksand/entropy_observations. Reading the whole chain in one request has never been the intended access pattern; follow the cursors.- An unrecognized query parameter is ignored, not rejected. This is uniform across the JSON:API surface. In particular the pre-pagination
?limit=parameter no longer exists on this route: passing it has no effect, and the request returns a default-size page. Usepage[limit].
GraphQL queries
The same four reads are available at the /gql endpoint as queries, with camelCase field names:
query ListBeacons {
beacons { id hash timestamp previousHash }
}
query LatestBeacon {
latestBeacon { id hash timestamp previousHash }
}
query BeaconById($id: String!) {
beacon(id: $id) { id hash timestamp previousHash }
}
query BeaconByHash($hash: String!) {
beaconByHash(hash: $hash) { id hash timestamp previousHash }
}
The beacons list returns the most recent beacons, newest first (up to 25). It takes no arguments: there is deliberately no filter, sort or pagination on this query, because a narrowing argument against a response that is always capped at 25 would quietly hide matches. A consumer that needs to filter, sort or walk the whole chain should use the JSON:API list endpoint, which has the cursors to do it correctly. Lookup misses and invalid arguments surface in the standard GraphQL errors envelope rather than as HTTP error statuses. The interactive playground described in the GraphQL concept is the quickest way to explore these queries live.
Errors
Error behavior on the JSON:API surface:
| Status | Cause |
|---|---|
401 |
Missing, blank, malformed, or invalid bearer credential. |
403 |
An OAuth access token that does not carry the api:read scope. API keys are not scope limited, so they never see this. |
400 |
Malformed :id (not a UUIDv7) or malformed :hash (not 64 hex characters); a page[after] / page[before] cursor that does not decode ("code": "invalid_keyset"); or a page[limit] below 1. |
404 |
No matching beacon: unknown id or hash, or a block that exists but is not yet finalized or committed. |
429 |
Per-caller rate limit exceeded. The error carries "code": "rate_limited" and meta.retry_after_ms; a Retry-After header accompanies it. |
Error bodies are JSON with an errors array. The 403 scope rejection is the exception: its body is empty and it carries a WWW-Authenticate: Bearer error="insufficient_scope" challenge header naming the scope that was required. On GraphQL, the equivalent failures arrive as entries in the response’s errors list, with the queried beacon field resolved to null.
Beacon proofs over the API
A beacon’s existence and its commitment to a public blockchain can be proven with a downloadable proof bundle, generated programmatically from the beacon’s id:
curl -s -X POST https://www.truestamp.com/api/json/proof/generate \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{"data": {"id": "<beacon-id>", "type": "beacon"}}'
typeis required and has no auto-detection."beacon"produces the beacon-flavored bundle (top-leveltypeof"beacon", code11);"block"produces the plain block bundle ("block", code10) for the same block. The two are structurally identical but cryptographically distinct, because the t type code is bound into the signed payload.- An optional
formatargument selects"json"(default) or"cbor"(base64-encoded CBOR binary). - An optional
witnessesargument chooses which witness details ride in the bundle. Omit it to carry every witness, so the file stands alone; pass[]to carry none, producing a compact bundle. The only witness that applies to a beacon (or a block) issigning_key_event, the ledger block that introduced the proof signer’s key, carried with that block’s own commitments; other names are accepted and ignored. A name outside the registry is rejected asinvalid_witness. - Proof generation requires the block to have at least one commitment on a public blockchain. A freshly finalized beacon sits in a brief window before the next epoch commit during which no proof can be generated yet.
- Unlike the beacon reads, the proof routes wrap their response in a
{"result": ...}envelope. - Both proof routes are
POSTonly because Ash mounts generic actions that way; they persist nothing, so an OAuth token needs only theapi:readscope, the same as the beacon reads.
The companion POST /api/json/proof/verify accepts a proof bundle plus optional type (assert the expected subject type), expected_hash, and skip_external (skip live Stellar and Bitcoin checks for offline or bounded-latency verification) arguments. The bundle’s wire format is specified in the proof bundle format, and the end-to-end procedure in how to verify a proof.
Other authenticated surfaces
Two more authenticated surfaces expose the same beacon reads:
- MCP code mode. The MCP server’s Lua manifest whitelists the same four beacon reads (list, get, latest, get by hash), so an agent can compose beacon lookups with other calls in a single script. Use the server’s Lua API docs tool to discover the exact call shapes.
- Console WebSocket. The developer console channel offers a global
beaconsstream that pushes abeacon.createdevent, carrying the same four fields, each time a new block is finalized. This is the push-based alternative to pollinglatest.
Examples
Fetch the current head beacon:
curl -s https://www.truestamp.com/api/json/beacons/latest \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" | jq
List the ten most recent beacons:
curl -s "https://www.truestamp.com/api/json/beacons?page\[limit\]=10" \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" | jq
Walk the chain from the genesis beacon forward, two at a time, and read the total:
curl -s "https://www.truestamp.com/api/json/beacons?sort=id&page\[limit\]=2&page\[count\]=true" \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" | jq '.meta.page.total, .links.next'
Look up a beacon by its hash:
curl -s https://www.truestamp.com/api/json/beacons/by-hash/<64-hex-hash> \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" | jq
The same head lookup over GraphQL:
curl -s -X POST https://www.truestamp.com/gql \
-H "Authorization: Bearer $TRUESTAMP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "{ latestBeacon { id hash timestamp previousHash } }"}'
A consumer walking the chain has two options. Repeated by-hash lookups follow previous_hash from any beacon until they reach the first block, whose previous_hash is a fixed sentinel value rather than a real predecessor; this verifies each link as it goes. Paging the list with sort=id covers the same ground in far fewer requests when the goal is to enumerate rather than to verify.