Support

We're here to help you get the most out of Truestamp. Find answers to common questions or get in touch with our support team.

Community & Updates

Share feedback, follow what we're building next, and see what just shipped. Each opens right here in a popup, so no separate account is needed.

Knowledge Base

Browse the concepts behind Truestamp. Pick a domain to explore, follow the links between concepts, or search across everything.

Console WebSocket Surface

The authenticated multiplexed console WebSocket at /console/websocket, its stream catalog, inbound commands (subscribe, items.create, items.watch), outbound stream events, first-event-immediate burst coalescer, and per-connection rate, stream, and payload limits.

Overview

The console WebSocket is an authenticated, multiplexed real-time surface at /console/websocket that backs the truestamp console terminal client and any other client you write by hand. Over a single connection it lets you subscribe to live streams of platform activity (block lifecycle, entropy observations, public chain commitments, and your team’s items) and issue a small set of commands, including submitting a new item for timestamping and watching its lifecycle. The socket always requires credentials: it accepts either an API key or an OAuth 2.1 bearer access token, and rejects any connection that presents neither. It speaks the Phoenix Channels V2 wire format (five-element JSON arrays), so it can be driven from a command-line WebSocket tool as easily as from a purpose-built client.

Connecting and authentication

The surface is a WebSocket upgrade at /console/websocket. You must pin the Phoenix Channels wire-protocol version in the query string as vsn=2.0.0; this selects the array-form serializer the surface speaks and is not a Truestamp API version.

Authentication is mandatory and can be supplied one of two ways:

  • API key as a query parameter on the upgrade URL, the same key format used by the JSON API:

    wss://www.truestamp.com/console/websocket?vsn=2.0.0&api_key=truestamp_<token>
    
  • OAuth 2.1 bearer access token. Browsers cannot set request headers on a WebSocket upgrade, so the token is normally supplied as an access_token query parameter, mirroring the api_key mechanism. An Authorization: Bearer <token> header is also accepted as a fallback for callers that can set headers on the upgrade. Either way, an OAuth connection must carry the console:read scope to open the socket at all; the console:write scope is checked separately, only when you issue a write command. OAuth is honored only while the server’s OAuth 2.1 authorization server is enabled; when it is off, only API-key connections open.

If neither a valid API key nor a valid token is presented, the upgrade is refused and the connection closes. On success the server binds your identity and current team to the connection, and every command and subscription runs authorized against that context. API-key connections stay open until you close them; OAuth connections are time-boxed to the access token’s expiry, at which point the server pushes a token_expired event and closes the channel so the client can reconnect with a fresh token.

Wire format and the lobby topic

The surface uses Phoenix Channels V2 framing: each message is a five-element JSON array of the form [join_ref, ref, topic, event, payload]. The ref is a correlation id you choose; the server echoes it on the matching reply so you can pair a phx_reply back to the request that produced it. Server-initiated pushes carry null for both refs.

All commands and stream events flow over one topic, console:lobby, which you join by sending a phx_join event. The join reply is a welcome envelope containing your resolved scope (user id, current team id, plan), the stream catalog available to you, and the server version. A separate console:clock topic on the same socket emits a one-per-second server time tick and is optional.

Malformed inbound frames (for example multi-line JSON split across several frames, or a stray blank line) are dropped cleanly rather than tearing down the connection, so hand-rolled debugging with a line-oriented tool is forgiving.

Keeping the connection alive

To hold the socket open through network middlebox idle timeouts, clients should periodically send a no-op heartbeat frame, roughly every 30 seconds, on the reserved phoenix topic:

[null, "<ref>", "phoenix", "heartbeat", {}]

The server acknowledges each heartbeat with {"status": "ok", "response": {}}. Purpose-built clients send this frame automatically, but a hand-rolled client must send it itself or the connection will silently drop after an idle period.

Stream catalog

Clients subscribe by stream id, never by an internal topic. The catalog is the stable contract. The streams available to every authenticated connection are:

Stream id Scope What it carries
blocks global Internal ledger block lifecycle (creation and later transitions).
blocks.healing global Block healing transitions (forward and reverse).
beacons global Public beacon projection of newly created blocks (id, hash, previous_hash, timestamp).
commitments.internal global Item Merkle inclusion commitments (each item bound into a block).
commitments.external global Block commitments recorded on public chains (Stellar, Bitcoin).
entropy.nist global NIST beacon entropy observations.
entropy.stellar global Stellar entropy observations.
entropy.bitcoin global Bitcoin entropy observations.
items.team team Item lifecycle events for your current team.
items.<item_id> per-item Lifecycle events for a single item you explicitly watch.

The items.team stream is only offered when your connection has a current team. The per-item items.<item_id> streams are dynamic: they appear when you watch an item and are removed when you unwatch it. Any stream id not in the catalog is rejected even for an authenticated caller.

Inbound commands

Every command is an event on console:lobby. The server answers with a phx_reply carrying {"status": "ok" | "error", "response": {...}}.

Command Payload On success
subscribe {"streams": [stream_id, ...]} {subscribed, rejected, active}
unsubscribe {"streams": [stream_id, ...]} {unsubscribed, active}
subscriptions {} {active} (the current active set)
ping {} {pong: <unix_ms>}
items.create {"name", "description", "hash"?, "hash_type"?, "watch"?} {item: <projection>, watched}
items.watch {"id": "<item_id>"} {watching: "<item_id>"}
items.unwatch {"id": "<item_id>"} {unwatching: "<item_id>"}
scope.switch_team {"team_id": "<team_id>"} {scope, team, role, streams}
token.refresh {"access_token": "<token>"} {exp: <unix>} (OAuth connections only)

A subscribe call reports which requested streams were accepted, which were rejected (unknown, not permitted, or over the per-connection stream cap), and the full active set after the call.

scope.switch_team re-binds the connection to another team you belong to without dropping the socket: catalog subscriptions are re-pointed at the new team while per-item watches keep their original binding, so you can watch an item that lives in a different team. token.refresh is an in-band keep-alive for OAuth connections: it adopts a freshly minted access token and reschedules the expiry so a long-lived session does not have to reconnect just because a token rolled over. It is a no-op on API-key connections, which never expire mid-session.

Submitting an item with items.create

items.create submits a new item for timestamping over the same connection. It has two modes, distinguished by whether you supply a hash:

  • External-hash mode: supply both hash and hash_type (for example a SHA-256 hex digest of a file you keep yourself). Only the digest and your descriptive claims are submitted; the file itself never leaves your machine.

    [null, "3", "console:lobby", "items.create", {
      "name": "Quarterly report",
      "description": "Q3 financials",
      "hash": "ab9d...",
      "hash_type": "sha256",
      "watch": true
    }]
    
  • Claims-as-source-of-truth mode: omit both hash and hash_type. The claims content itself (name, description, and any metadata) becomes the timestamped data. In this mode the description must be at least 32 characters, or the claims must include non-empty metadata, so there is real content to timestamp; the total claims payload is still bounded by the team owner’s plan size budget for claims-only submissions.

    [null, "3", "console:lobby", "items.create", {
      "name": "Invention disclosure",
      "description": "On this day I claim the following invention as my own work.",
      "watch": true
    }]
    

hash and hash_type are co-required: supply both or neither. When watch: true, the connection also subscribes to the new item’s lifecycle so its state transitions arrive as stream pushes on items.<item_id>. The reply’s watched field reports whether that auto-watch actually took effect; the item is created either way. The resulting item proves the submission window for its data, not a creation time. On a validation or authorization failure the reply is {"status": "error", "response": {"code": ..., "message": ...}}.

Because writing is a privileged action, items.create on an OAuth connection requires the console:write scope; without it the reply is {"code": "insufficient_scope"}. API-key connections are not scope-limited.

Watching an item with items.watch

items.watch adds a per-item subscription after confirming your credentials can read the item; if the item does not exist or you cannot read it, the reply is {"code": "forbidden"} (the two cases are deliberately indistinguishable). Once watching, the item’s lifecycle arrives as stream pushes on stream id items.<item_id>. items.unwatch removes it. If the connection is already at its per-connection stream cap, the watch is refused with {"code": "cap_exceeded"}.

Outbound stream events

Every server-initiated resource event on console:lobby is a stream event with a uniform envelope, so a client can route on kind without inspecting the shape of data:

[null, null, "console:lobby", "stream", {
  "stream": "blocks",
  "kind":   "block.created",
  "at":     "2026-04-27T18:32:11.412Z",
  "data":   { "id": "019d...", "state": "finalized", "block_hash": "61b8...", "merkle_root": "657b..." }
}]

The kind always follows <resource>.<verb>, for example block.created, beacon.created, commitment.created, external_commitment.created, entropy.created, and item.created / item.updated / item.deleted. The data map is a small, stable projection of the underlying resource rather than the raw record, so its shape does not shift when internal fields change. Hash and key-id fields are lowercase hex strings.

Burst coalescing

Every stream passes through one uniform coalescer so a fast fan-out cannot flood a client. The first event in a short window is pushed immediately, so slow streams pay no latency penalty. Further events that arrive during that window accumulate. When the window closes:

  • If nothing else accumulated, nothing more is pushed.
  • If exactly one further event accumulated, it is promoted to an ordinary individual push (never a burst-of-one).
  • If two or more accumulated, a single <resource>.burst summary is pushed instead.

A burst summary carries the count, the window length, first and last timestamps, per-kind and per-state histograms, and a projection of the latest event:

[null, null, "console:lobby", "stream", {
  "stream": "items.team",
  "kind":   "item.burst",
  "at":     "2026-04-27T18:32:11.500Z",
  "data": {
    "count": 437,
    "window_ms": 500,
    "first_at": "2026-04-27T18:32:11.001Z",
    "last_at":  "2026-04-27T18:32:11.498Z",
    "by_kind":  { "item.created": 250, "item.updated": 150, "item.deleted": 37 },
    "by_state": { "created": 200, "processing": 150, "committed": 87 },
    "latest":   { "id": "01KQ...", "state": "committed" }
  }
}]

The effect is adaptive: quiet streams like blocks, entropy.*, and commitments.external never produce a burst because their events rarely arrive fast enough to accumulate, while high-volume streams like items.team during a block close self-throttle to at most a couple of pushes per second regardless of input rate.

Per-connection limits

The surface applies several defense-in-depth caps that should never fire under normal interactive use:

  • Stream cap: a single connection can hold up to 32 active streams (catalog streams plus per-item watches). Requests past the cap are reported in rejected (for subscribe) or as cap_exceeded (for a watch).
  • Per-call stream cap: a single subscribe or unsubscribe may name at most 64 stream ids; a larger array is rejected as invalid.
  • Command rate limit: inbound commands are rate limited per connection over a short window. Exceeding it replies {"code": "rate_limited"}.
  • Outbound rate limit: outbound stream pushes are capped per second; excess pushes are dropped server-side rather than delivered.
  • Payload size guard: an oversized projected data payload is replaced with a truncation marker carrying just the id and the original byte size, so a single large record cannot breach the frame budget.
  • Frame size and idle memory: the socket caps inbound frame size and hibernates idle connections to shrink their memory footprint.

Treat the exact numbers as tuning parameters that can change; the behaviors above are the contract.

Limitations

The console WebSocket does not verify proofs or expose bulk history; it is a real-time command and subscription surface. It carries live events from the moment you subscribe and does not replay past events, so a client that reconnects must re-issue its subscriptions and treat any gap as an outage to backfill through other means. It exposes only the item write actions listed here (create, watch, unwatch, team switch); richer read and management operations belong to the JSON API and GraphQL surfaces.

Get Help

API Documentation

Comprehensive guides for the REST and GraphQL APIs, including interactive documentation and code examples.

View API Docs

FAQ

Quick answers to the most commonly asked questions about timestamping and verification.

Browse FAQ

Email Support

Send us a message and our team will respond within 24 hours.

[email protected]

Security Issues

Report security vulnerabilities through our responsible disclosure program.

[email protected]