Ledger
Append hashes of your app's records, so you can show later that a value was already settled at a given time.
Only a hash made by your app (c) goes into the ledger. Do not send the record itself, user identifiers or timestamps. Keep the record and its salt in your app or in the Vault.
Developer console
1. Open your Org, or create one if you have none.
2. Subscribe the Org to Ledger.
3. Register the app and confirm its host (see the Quickstart).
4. On the app page, add Ledger to its declared use.
Authentication
Your app's server signs a JWT with the operate key for every request. Session JWTs are not used here.
JWT claims
{
"iss": "app:<app_id>",
"aud": "clavis-ledger",
"app_id": "<app_id>",
"htm": "POST",
"htu": "/archive/ledger/v1/entries",
"body_sha256": "<SHA-256 of the body (hex); omit when there is no body>",
"iat": 1790000000,
"exp": 1790000060,
"jti": "<a value used only once>"
}htuThe path and query, starting at /archive.exp − iat300 seconds at most.jtiThe same value is never accepted twice.
Commitment
salt = 32 random bytes (new for every record)
c = SHA-256( "clavis.commit.v1" ‖ salt ‖ canonical_json(record) )
slot = HMAC-SHA256( your app's secret key, "asset:<asset_id>:v<version>" ) // optionalsaltRandom, new for every record. Reusing it lets others guess the record.canonical_jsonJSON with keys sorted in ascending order.slotStops a second record from being settled for the same version of the same asset. Your app's secret key never goes to Clavis.
Opening (keep it in your app or the Vault)
{
"salt": "<base64url, 32 bytes>",
"record": { "kind": "transfer", "asset": "a-1", "from": "u1", "to": "u2" }
}Steps
1. Make c from the record and the salt. For a transfer of ownership, make a slot too.
2. Send the leaf, and keep the receipt from the response with your record.
RequestPOST https://api.clavis.nexus/archive/ledger/v1/entries Authorization: ClavisApp <JWT> Content-Type: application/json { "leaf": { "v": 1, "c": "<64 hex>", "slot": "<64 hex (optional)>" } }201 Response
{ "log_id": "<Org UUID>", "status": "created", "index": 41, "leaf_hash": "<64 hex>", "receipt": "clavis.ledger.receipt/v1\nclavis.nexus/ledger/<Org UUID>\n41\n<base64>\n\n— clavis-ledger-log <base64>\n" }3. After about a minute, fetch the inclusion proof once the leaf is in a checkpoint. Until then you get 202.
RequestGET https://api.clavis.nexus/archive/ledger/v1/proof?index=41 Authorization: ClavisApp <JWT>200 Response
{ "log_id": "<Org UUID>", "index": 41, "size": 42, "checkpoint": "clavis.nexus/ledger/<Org UUID>\n42\n<base64 root>\ntime 1790000060\n\n— clavis-ledger-log <base64>\n", "inclusion": ["<base64>", "<base64>"] }4. To hand it to someone else, fetch the proof bundle and add the opening. An external timestamp (the anchor) is attached within about an hour.
RequestGET https://api.clavis.nexus/archive/ledger/v1/bundle?index=41 Authorization: ClavisApp <JWT>200 Response
{ "anchor_status": "stamped", "bundle": { "version": 3, "log_id": "<Org UUID>", "leaf": { "v": 1, "c": "<64 hex>" }, "index": 41, "receipt": "<signed note>", "checkpoint": "<signed note>", "inclusion": ["<base64>"], "anchor": { "index": 7, "checkpoint": "<signed note>", "inclusion": ["<base64>"], "tsa": [{ "url": "<TSA>", "response": "<RFC 3161 response (base64)>" }] }, "jwks": { "keys": [], "master": {} } } }
Reading
GET https://api.clavis.nexus/archive/ledger/v1/checkpoint
Authorization: ClavisApp <JWT>GET https://api.clavis.nexus/archive/ledger/v1/consistency?first=<size>&second=<size>
Authorization: ClavisApp <JWT>GET https://api.clavis.nexus/archive/ledger/v1/anchor-proof?size=<size>
Authorization: ClavisApp <JWT>GET https://api.clavis.nexus/archive/ledger/v1/entries/<index>
Authorization: ClavisApp <JWT>GET https://api.clavis.nexus/archive/ledger/v1/entries?c=<64 hex>
Authorization: ClavisApp <JWT>- An app can read only the log of the Org it belongs to.
- Reading and fetching proof bundles still work after you cancel.
Vault
Storage so you don't lose openings. Encrypt them in your app before storing. You can delete them independently of the ledger.
PUT https://api.clavis.nexus/archive/vault/v1/objects/<object_id>?subject_tag=<64 hex>&expires_at=<unix>
Authorization: ClavisApp <JWT>
Content-Type: application/octet-stream
<ciphertext>GET https://api.clavis.nexus/archive/vault/v1/objects/<object_id>
Authorization: ClavisApp <JWT>DELETE https://api.clavis.nexus/archive/vault/v1/objects/<object_id>
Authorization: ClavisApp <JWT>DELETE https://api.clavis.nexus/archive/vault/v1/subjects/<subject_tag>
Authorization: ClavisApp <JWT>object_id16 to 128 letters, digits, hyphens or underscores. It cannot be overwritten.subject_tagOptional. Use a value Clavis cannot tie to a person, such as an HMAC of your app's user ID.expires_atOptional. Expired objects are deleted daily.
Verification
- A proof bundle can be verified even while Clavis is down. All you need is the bundle and the master public key from the aggregate JWKS.
- Receipts and checkpoints are C2SP signed notes (Ed25519); inclusion and consistency proofs follow RFC 9162; timestamps follow RFC 3161.
- With the opening you can recompute c and check the contents of the record itself.
- To check in a browser, paste the bundle into
archive.clavis.nexus/verify. The bundle is not sent anywhere.
Limits
- Each new leaf counts as one use of the Ledger plan. Resending the same c and reading are not counted.
- Appending leaves and storing in the Vault need a Ledger subscription and remaining allowance this month.
- Up to 600 requests per minute per app.
- The Vault holds up to 256 KiB per object and 1 GiB per Org in total.
Errors
Every error has error, message, requestId and trace_id.
401 APP_AUTH_FAILEDThe JWT could not be verified. Fix what message names (signature, claims, expired, body_mismatch and so on).409 DUPLICATE_JTIThat jti has been used. Make a new JWT for every request.409 APP_NOT_ACTIVEThe app's host is not confirmed.402 CARD_CLOSEDIf message is declaration, Ledger is not in the declared use. Otherwise check the subscription or this month's allowance.400 INVALID_LEAFA leaf has only v, c and an optional slot. c and slot are lowercase 64-character hex.409 SLOT_TAKENA different c is already settled in that slot. Move to the next version, then send again.202 pendingNot in a checkpoint or timestamp proof yet. Fetch again later.409 OBJECT_EXISTSAn object with that object_id exists. Delete it before storing again.413 OBJECT_TOO_LARGE / 507 VAULT_QUOTA_EXCEEDEDOver a Vault limit.429 RATE_LIMITEDOver the per-minute limit.
Reference
- Quickstart — Setting up the Org, the app and the host check.
- Authentication — How the operate key differs from the session JWT.