Skip to content

API integration

The goal: nobody opens a console to record each step. An official authorizes your software once; the institution’s own system then records every checkpoint — a case opened, an inspection, an approval, a payment — and hands the citizen a Receipt. Writes need no API key: an event is accepted only if it extends its issuer’s HardState Log and is signed by a key that log authorizes.

Everything below uses the public API at https://api.hardstate.net (http://localhost:8787 when running locally).

Signer When to use How it is authorized Attributed to
Session of a person (role: session) A workstation, tablet, or field app during someone’s shift The person approves it once with their passkey (session.open); it expires (≤ 30 days) That person, by name
System key (role: system) A process with no person behind it: a payment gateway, an appointment system, a scanner An admin registers it once (key.add), scoped to some step types The system, by name
Passkey, step by step Low volume, or high-stakes acts signed in person Already registered in the institution’s log That person

Prefer sessions for anything a person is responsible for — accountability stays personal, and a lost device is closed in one click. Use system keys only for steps no person performs, and always scope them ("types": ["fee.paid"]). See Officials & sessions for how these are created.

Each step is an event of a type you choose, written domain.action in lowercase: case.open, case.inspect, permit.approve, permit.deny, fee.paid, diploma.issue, aid.delivered, contract.award, land_title.transfer. Only issuer.*, key.*, and session.* are reserved by the protocol. Keep one type per kind of act, put the act’s public facts in public, and use case for the file or procedure number — so a citizen’s whole procedure reads as one timeline.

Your software generates its own Ed25519 key pair and keeps the private key — a file with 0600 permissions, or better an HSM or a cloud KMS. It then asks to be authorized with a request link:

https://app.hardstate.net/console?device=<base64url(JSON)>
JSON = { "v": 1, "label": "Permits system — City Hall",
"publicKey": "<SPKI DER, base64url>", "types": ["case.*", "permit.*"] }

Open the link while signed in to the console (Devices & sessions):

  • an official approves it as their session, for a chosen time; or
  • an admin registers it as a system key, valid until revoked.

The key id your software uses as actor is:

actor = "ed25519:" + hex(SHA-256(spki))[0:32]

4. Record each checkpoint: prepare → sign → submit

Section titled “4. Record each checkpoint: prepare → sign → submit”

Any language that can make an HTTP request and an Ed25519 signature can integrate. No canonical-JSON library is needed — the API prepares the exact event for you.

  1. Prepare. POST /v1/events/prepare with the step: { issuer, type, case, public, docs, actor }. It answers { event, canonical, hash, challenge }, where event sits on the current head of your log.

  2. Check, then sign. Compare event against what you sent, then sign the 32 bytes of hash with your Ed25519 key. Signing only what you meant is the point of this step.

  3. Submit. POST /v1/events with { event, sig: { kind: "ed25519", signature: <base64url> } }. It answers 201 with { hash, seq, receipt, proof }. On 409 another step got there first — prepare again on the returned head.

Terminal window
# 1. Prepare
curl -s https://api.hardstate.net/v1/events/prepare -H 'content-type: application/json' -d '{
"issuer": "city-of-esperanza", "type": "permit.approve", "case": "BP-2026-0042",
"public": { "decision": "approved", "validUntil": "2027-10-20" }, "actor": "ed25519:4be1…"
}'
# 2. Sign the returned .hash (32 bytes) with your Ed25519 key, then:
# 3. Submit
curl -s https://api.hardstate.net/v1/events -H 'content-type: application/json' \
-d '{ "event": { … the event returned by prepare … },
"sig": { "kind": "ed25519", "signature": "…" } }'

Never send documents to HardState unless the institution decides to publish them. Commit to each file instead:

digest = SHA-256(file)
salt = 32 random bytes (keep it: it goes in the citizen's receipt)
commitment = SHA-256("hardstate/doc/v1" ‖ salt ‖ digest)
docs = [{ "label": "Inspection report", "commitment": hex(commitment), "mime": "application/pdf" }]

Give the citizen the receipt link with the salts in its fragment: <receipt>#k=<commitment[0:16]>.<salt hex>,…. The fragment never reaches any server; with it, anyone holding the file can check it is the recorded one. To publish a document on purpose, send publish: [{ commitment, digest, salt }] with the event, then PUT /v1/docs/<commitment> with the file bytes.

POST /v1/events returns a receipt link (https://app.hardstate.net/r/<hash>). Append the #k=… fragment when the step has documents, and put the link and its QR into what the citizen already receives — email, SMS, the printed form. Every step of the case links to the full timeline, and anyone can verify it against Bitcoin.

  • Custody. System and session private keys never leave the machine that uses them (0600, HSM, or KMS).
  • Scope. Give each key the narrowest set of types that works.
  • Retries. 409 means the log head moved — prepare again. Network errors are safe to retry with the same signed event: a duplicate is refused, not recorded twice.
  • Pace. Records reach Bitcoin within 1–3 hours through OpenTimestamps (free) and in the daily HardState Anchor. Express anchoring (~10–20 min) is available on institutional plans.