Developer console

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. 1. Open your Org, or create one if you have none.

  2. 2. Subscribe the Org to Ledger.

  3. 3. Register the app and confirm its host (see the Quickstart).

  4. 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>"
}
  • htu
    The path and query, starting at /archive.
  • exp − iat
    300 seconds at most.
  • jti
    The same value is never accepted twice.

Commitment

Computed on your app's server
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>" )   // optional
  • salt
    Random, new for every record. Reusing it lets others guess the record.
  • canonical_json
    JSON with keys sorted in ascending order.
  • slot
    Stops 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. 1. Make c from the record and the salt. For a transfer of ownership, make a slot too.

  2. 2. Send the leaf, and keep the receipt from the response with your record.

    Request
    POST 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. 3. After about a minute, fetch the inclusion proof once the leaf is in a checkpoint. Until then you get 202.

    Request
    GET 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. 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.

    Request
    GET 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

Latest checkpoint
GET https://api.clavis.nexus/archive/ledger/v1/checkpoint
Authorization: ClavisApp <JWT>
Consistency proof between two checkpoints
GET https://api.clavis.nexus/archive/ledger/v1/consistency?first=<size>&second=<size>
Authorization: ClavisApp <JWT>
Timestamp proof for a checkpoint
GET https://api.clavis.nexus/archive/ledger/v1/anchor-proof?size=<size>
Authorization: ClavisApp <JWT>
Leaf at an index
GET https://api.clavis.nexus/archive/ledger/v1/entries/<index>
Authorization: ClavisApp <JWT>
Index for a c
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.

Store
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>
Fetch
GET https://api.clavis.nexus/archive/vault/v1/objects/<object_id>
Authorization: ClavisApp <JWT>
Delete
DELETE https://api.clavis.nexus/archive/vault/v1/objects/<object_id>
Authorization: ClavisApp <JWT>
Delete by subject
DELETE https://api.clavis.nexus/archive/vault/v1/subjects/<subject_tag>
Authorization: ClavisApp <JWT>
  • object_id
    16 to 128 letters, digits, hyphens or underscores. It cannot be overwritten.
  • subject_tag
    Optional. Use a value Clavis cannot tie to a person, such as an HMAC of your app's user ID.
  • expires_at
    Optional. 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_FAILED
    The JWT could not be verified. Fix what message names (signature, claims, expired, body_mismatch and so on).
  • 409 DUPLICATE_JTI
    That jti has been used. Make a new JWT for every request.
  • 409 APP_NOT_ACTIVE
    The app's host is not confirmed.
  • 402 CARD_CLOSED
    If message is declaration, Ledger is not in the declared use. Otherwise check the subscription or this month's allowance.
  • 400 INVALID_LEAF
    A leaf has only v, c and an optional slot. c and slot are lowercase 64-character hex.
  • 409 SLOT_TAKEN
    A different c is already settled in that slot. Move to the next version, then send again.
  • 202 pending
    Not in a checkpoint or timestamp proof yet. Fetch again later.
  • 409 OBJECT_EXISTS
    An object with that object_id exists. Delete it before storing again.
  • 413 OBJECT_TOO_LARGE / 507 VAULT_QUOTA_EXCEEDED
    Over a Vault limit.
  • 429 RATE_LIMITED
    Over 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.