Skip to content

API reference

This page is a quick reference, not the standard. The normative text is HSS-1, and the machine-readable API contract is openapi.yaml. Where this page and the standard disagree, the standard wins.

Thing Rule
Hashes SHA-256, written as lowercase hex (64 characters).
Binary in JSON base64url, no padding (signatures, keys, WebAuthn data).
Canonical JSON RFC 8785 (JCS). canonical(x) is the UTF-8 bytes of the JCS serialization.
Times RFC 3339 UTC strings, e.g. 2026-10-20T15:18:38.955Z.
{
"hss": 1,
"issuer": "esperanza-demo",
"seq": 5,
"prev": "09f54f6f…69b9ab",
"type": "case.inspect",
"case": "BP-2026-4077",
"ts": "2026-10-20T15:18:39.120Z",
"public": { "result": "pass", "inspector": "Unit 3" },
"docs": [{ "label": "Inspection report", "commitment": "…", "mime": "application/pdf", "size": 812 }],
"actor": "ed25519:4be1…"
}
Field Rule
hss MUST be 1.
issuer Lowercase slug ^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$. Stable for the life of the log.
seq 0 for the first event, then exactly previous seq + 1.
prev null at seq 0; otherwise the event hash of seq - 1.
type ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$. Types below are reserved.
case OPTIONAL, up to 128 characters. Groups the events of one procedure.
ts When the issuer states the act happened. Untrusted; the Bitcoin anchor bounds it from above.
public JSON object, published as-is. Never put personal data here.
docs Array of document references (may be empty).
actor Key id of the signer.

The event hash is SHA-256(canonical(event)); the signature is not part of the event. A serialized event MUST NOT exceed 64 KiB, and unknown top-level fields are rejected under hss: 1.

{ "event": { … }, "sig": { "kind": "passkey" | "ed25519", … } }
  • ed25519 (system-to-system): sig.signature is the signature over the 32 raw bytes of the event hash.
  • passkey (people): a WebAuthn assertion (authenticatorData, clientDataJSON, signature) produced with challenge = event_hash, verified per HSS-1 §3.3.

Keys live in the issuer’s own log; there is no separate key registry.

{ "id": "…", "kind": "passkey" | "ed25519", "alg": -7 | -257 | -8,
"publicKey": "<SPKI DER, base64url>",
"role": "admin" | "official" | "system" | "session",
"label": "Ana López — Building inspector",
"rpId": "app.hardstate.net",
"scope": { "types": ["case.*", "fee.paid"], "expires": "2026-10-20T20:00:00Z" } }
  • Passkeys: id is the WebAuthn credential id (base64url); rpId is REQUIRED; alg is -7 (ES256) or -257 (RS256).
  • Ed25519 keys: alg MUST be -8; id is "ed25519:" + hex(SHA-256(spki))[0:32].
  • scope.types: exact types or a trailing wildcard ("case.*"); MUST NOT grant system types. scope.expires: the last event.ts the key may sign.
  • Roles: admin and official are people; system is a person-less process and MUST carry scope.types; session keys exist only through session.open. system and session keys MUST NOT sign issuer.*, key.*, or session.open.
Type What it does Signed by
issuer.create seq 0. Declares public.name, optional jurisdiction / location / website, and public.key (the founding admin key). the key it declares
key.add Adds a key in public.key. an active admin
key.revoke Revokes public.keyId. The last active admin MUST NOT be revoked. an active admin
issuer.update Merges profile changes into the issuer’s profile. an active admin
session.open Declares a session key in public.key; scope.expires ≤ 30 days. an active admin or official
session.close Closes the session named by public.keyId. the opener, the session itself, or an admin

Every other event type is the issuer’s own vocabulary (domain.action).

commitment = SHA-256("hardstate/doc/v1" ‖ salt ‖ SHA-256(file)) salt: 32 random bytes

Publishing a document reveals its salt and SHA-256(file) and makes the bytes available. Receipts carry salts in the URL fragment (/r/<event_hash>#k=<commitment[0:16]>.<salt>,…), which browsers never send to a server.

Layer Fact
Batches RFC 9162 (CT v2) Merkle tree; leaf data 0x01 ‖ event_hash (events) or 0x02 ‖ digest (stamps), reference interval 10 minutes.
OpenTimestamps Every batch root → ≥ 2 public calendars; a standard detached .ots (ots verify -d <root> <file>.ots).
HardState Anchor Daily. OP_RETURN PUSH36("HSS" ‖ 0x01 ‖ root), script 6a 24 48 53 53 01 <32 bytes> — 36 bytes of payload, 157 vB. Anchor-tree leaf data is 0x03 ‖ batch_root.

The daily anchor costs roughly $48–240/year at typical fee rates (about 1–5 sat/vB). Signet anchors are labelled as tests and never count as a Bitcoin proof.

Base URL https://api.hardstate.net (http://localhost:8787 locally). Reads are open and keyless. Writes need no API key — an event is accepted only if it extends its issuer’s log and is signed by an authorized key.

Method Path Does
GET /v1/health Liveness and anchoring network.
GET /v1/stats Counts, last batch, last anchor.
GET /v1/activity?limit= Latest events across issuers.
GET /v1/issuers All issuers with their head and domain verification.
GET /v1/issuers/{id} Profile, keys (as declared in the log), head, and cases.
GET /v1/issuers/{id}/log?from=&limit= The signed HardState Log, in order; follow next until null.
GET /v1/issuers/{id}/cases/{case} All events of one case.
POST /v1/issuers/{id}/verify-domain Link a domain via /.well-known/hardstate.json.
POST /v1/events/prepare Prepare the next event on the live head → { event, canonical, hash, challenge }.
POST /v1/events Append a signed event.
GET /v1/events/{hash} One signed event, its pipeline status, and published documents.
Method Path Does
GET /v1/proofs/{hash} HardState Proof + status: queued → batched → timestamped → anchored.
POST /v1/stamp Anchor a bare SHA-256 digest, with no account.
GET /v1/batches, /v1/batches/{id} Recent batches / one batch with its ordered leaf list.
GET /v1/batches/{id}/ots The detached .ots for the batch root.
GET /v1/anchors, /v1/anchors/{id} HardState Anchor transactions with fees / one anchor and its batches.
GET /v1/wallet Anchoring address, balance, and current cost per anchor.
Method Path Does
GET /v1/keys/{id} A signing key and its issuer.
PUT /v1/docs/{commitment} Upload a published document (its SHA-256 must match the published digest; ≤ 25 MB).
GET /v1/docs/{commitment} Download a published document (salt and digest in X-HardState-Salt / X-HardState-Digest).
GET /v1/lookup/{digest} Published documents (and stamps) with this plain SHA-256.
Code Meaning
201 Appended: { hash, issuer, seq, status: "queued", proof, receipt }.
404 The issuer does not exist and the event is not issuer.create.
409 The log head moved: { error, head }. Rebuild on the returned head and sign again.
422 Shape, chain, authorization, or signature check failed.
429 Rate limited.