Running a node

API

Every route the node serves, what it accepts, and what it answers. The eight /dedi/lookup|query|versions routes are the DeDi standard's read API (api/openapi.yaml); everything else is this node's extension, and conformance says which is which.

Responses use the standard's envelope: {"message", "data"} on success, with a proof beside data when one was asked for, and {"message", "error", "code"} on failure.

Read routes

Route Query parameters Returns
GET /dedi/lookup/{ns} version_id, as_on, proof, include_revoked, internal a namespace
GET /dedi/lookup/{ns}/{reg} same a registry, with its schema
GET /dedi/lookup/{ns}/{reg}/{rec} same a record; its payload is data.details
GET /dedi/query/{ns} name, status, state, from, to, as_on, sort, page, page_size, internal a page of the namespace's registries
GET /dedi/query/{ns}/{reg} same; or domain (with only internal) a page of the registry's records; with ?domain=, who serves that domain (discovery)
GET /dedi/versions/{ns}[/{reg}[/{rec}]] internal every version id, oldest first
GET /dedi/log/checkpoint — the latest signed checkpoint, text/plain
GET /dedi/log/proof/consistency old, new (1 ≤ old ≤ new ≤ tree size) an RFC 6962 consistency proof
GET /dedi/log/history buckets (48), bucket_seconds (1800, min 60) checkpoints and writes over time; what /status draws
GET /dedi/witness — every verdict this node holds about other logs (witnessing)
GET /dedi/witness/{origin} — one target's verdict, with the inclusion proof of the verdict itself; {origin} path-escaped
GET /dedi/delegations/{ns} — children delegated under {ns}, with witness verdict and loop health (delegation)
GET /dedi/network — peers, reachability, witness health, cluster state
GET /dedi/stats — requests served by status class, uptime
GET /.well-known/dedi.index.json — the signed DeDi manifest (file publication)
GET /dedi-files/{ns}/dedi.{reg}.json — one signed DeDi file
GET /healthz — flat JSON: database reachability, tree size, checkpoint age, cluster role; 503 only if the database is unreachable
GET /metrics — Prometheus text: replica lag, peer reachability

HTML pages: / (overview), /browse, /network, /status, /check, /verify, /docs, /docs/{page}, and /admin when the write plane is open.

Strict query parameters

The lookup, query and versions routes refuse a query they would otherwise answer by quietly ignoring part of it. Each of these is a 400:

Why: ?versionId=2 used to return the latest version, with a valid proof attached. Every check a careful client ran passed, because they were all about the record the node chose to return rather than the one asked for.

Parameter meanings

Now, or settled?

A lookup is a settled read when it pins a version_id, or asks for an as_on more than one minute in the past. Its answer cannot change, so it is served Cache-Control: immutable for a year, and a revoked record still answers (history stays readable).

Anything else is a now read: no as_on, an as_on of today (end of day), in the future, or under a minute ago. It is cached for the record's ttl only, because that is how a revocation reaches a consumer, and a revoked record answers 404 unless include_revoked=true. The one-minute margin covers a write whose timestamp was stamped just before it committed.

Every lookup carries an ETag; send it back as If-None-Match for a 304.

Versions, three numbers

Field Is Used for
version_id (also version) the log sequence number of that version ?version_id=
version_num 1, 2, 3… within this one resource reading, and the Merkle leaf
version_tag <sha256 of the payload>-<state> If-Match on the next write

A write's response carries all three, so the next request never has to reconstruct one.

Write routes

Registered only when DEDI_PUBLISHER_KEYS is set; otherwise they, and /admin, answer 404. Every one needs a publisher signature (DeDi-Key-Id, DeDi-Timestamp, DeDi-Signature, from dedid sign or the console) by a key scoped to {ns}, and the Basic-auth gate when DEDI_ADMIN_PASSWORD is set. Bodies are JSON.

Route Body Precondition
PUT /admin/namespaces/{ns} {"payload": {…}} required
PUT /admin/namespaces/{ns}/registries/{reg} {"payload": {…}}; "schema": "builtin:<name>" picks a reference schema required
POST …/registries/{reg}/records/{rec}/publish {"payload": {…}}, validated against the registry's schema required
POST …/registries/{reg}/records/{rec}/revoke optional {"reason": "…"} If-Match
POST …/registries/{reg}/subscriptions {"target_url": "https://…"} none
GET /admin/namespaces/{ns}/subscriptions — none
DELETE /admin/namespaces/{ns}/subscriptions/{id} — none
POST /admin/namespaces/{ns}/children {"namespace": "{ns}.child", "role": "child", …} none
POST /admin/namespaces/{ns}/children/{child}/revoke — none
POST /admin/node-config role and its inputs none
GET /admin/namespaces/{ns}/domain — none
POST /admin/namespaces/{ns}/domain/verify — none
DELETE /admin/namespaces/{ns}/domain — none

"Required" means If-None-Match: * (must not exist yet) or If-Match: <version_tag> (replace exactly this version). Both headers are inside the signature, so a captured request cannot be replayed without its precondition, and a replay lands as a 412 once the version has moved on. Publishing a record identical to its current version, or revoking one that is already revoked, is a no-op 200 with "unchanged": true rather than a duplicate version.

POST /enrol is the one write that is not signed: a child presents its one-time token there (delegation). POST /dedi/enrol is a deprecated alias.

Errors

One mapper handles every write failure, so the same cause gets the same answer on every route.

Status code When
400 INVALID_REQUEST bad query (above), malformed body, a payload the schema rejects, malformed signature headers
401 UNAUTHORIZED unsigned, unknown key id, bad signature, timestamp more than 5 minutes off; or the Basic-auth gate
403 FORBIDDEN the key is scoped to another namespace
404 NOT_FOUND no such resource or version; a hidden _ namespace; a write under a namespace or registry that does not exist (the message names it)
409 CONFLICT a delegation state that will not change by retrying: already delegated, offer already redeemed
412 PRECONDITION_FAILED the If-Match / If-None-Match did not hold; the message carries the current version_tag
428 PRECONDITION_REQUIRED a write that needs a precondition sent none
500 INTERNAL a fault on the node

On a replicated node only the leader writes. Every write that reaches a follower and passes the admin gate and signature check is answered 307 Temporary Redirect to the same path on the leader, method and body intact, so a client that follows redirects needs little cluster awareness. When there is no leader, or the leader's public URL is not configured in DEDI_CLUSTER_PEERS, the follower answers 503 with code NO_LEADER and Retry-After: 2. Reads are served by every replica.

← All documentation