Quick start: notarize an agent action and verify it offline

You have a running Server and an agent API key. In the next few minutes you will notarize what an agent is about to do, notarize what it did, and then verify one of those records on your own machine using nothing but the Server's published public key. That last step is the point: a record is not "trust us, it is in our database" — it is a signed artifact anyone can check without us.

If you do not yet have a Server, see the install guide. If you do not yet have an agent key, see authentication.

The model in three sentences

An agent notarizes what it is about to do, then notarizes what was done; each call returns a signed, tamper-evident record. Between the two an agent can reset its context, hand off, or be replaced — the records stand on their own, byte for byte. Anyone holding the Server's public key can verify each record offline, with no access to the Server, its database, or its network. (For the underlying data model, see records and the chain.)

Set your two inputs

export AGLEDGER_URL=https://your-server.example     # your Server
export AGLEDGER_KEY=agl_agt_…                        # your agent key

Confirm the key resolves before going further:

curl -s -H "Authorization: Bearer $AGLEDGER_KEY" "$AGLEDGER_URL/v1/auth/me"

This page uses notarize-generic-v1, the example notarize-only contract type seeded into each new org by default. List the types available on yours with GET /v1/schemas; fetch any type's schema and a copy-pasteable example with GET /v1/schemas/{type}.

1. Notarize what the agent is about to do

curl -s -X POST -H "Authorization: Bearer $AGLEDGER_KEY" -H "Content-Type: application/json" \
  -d '{"type":"notarize-generic-v1","criteria":{"summary":"About to reconcile invoice INV-4471 against PO-9921"}}' \
  "$AGLEDGER_URL/v1/records"
{
  "id": "019e623f-ad7f-7d0f-a623-67a2df7f1777",
  "status": "RECORDED",
  "type": "notarize-generic-v1",
  "signedStatement": {
    "chainPosition": 1,
    "leafHash": "c76de57bb39a7a82563ecfa3d10d0d411edbc35805919c1810ee89c8276653d1",
    "previousHash": null,
    "signingKeyId": "3fecdcafbc1a8b56",
    "signedCheckpointRef": null,
    "url": "/v1/records/019e623f-ad7f-7d0f-a623-67a2df7f1777/attestation"
  }
}

The record is RECORDED and already signed: signedStatement gives its hash, the key that signed it, and the URL of the signed envelope you will verify in step 3. As an agent key, you did not have to name the org or the principal — the Server resolved both from your key. Save the id.

2. Notarize what was done

Between these two calls the agent can do the work, lose its context, or hand off to another process. The first record already stands on its own; the second is a second standalone record. Notarize the outcome the same way:

curl -s -X POST -H "Authorization: Bearer $AGLEDGER_KEY" -H "Content-Type: application/json" \
  -d '{"type":"notarize-generic-v1","criteria":{"summary":"Reconciled invoice INV-4471 against PO-9921: matched, variance $0.00"}}' \
  "$AGLEDGER_URL/v1/records"

You now hold two signed records — what was intended and what happened — each independently verifiable. (notarize-generic-v1 is notarize-only: a record terminalizes at RECORDED on creation, with no later completion phase. Types that add a completion-and-verdict phase are a separate step — see what is next, below.)

3. Verify a record offline — the point of the exercise

Verification needs two things and no running Server: the record's signed envelope and the Server's public key. Both are served unauthenticated for the key; the envelope is read with your key.

# the record's signed envelope — served as application/cose-sequence
curl -s -H "Authorization: Bearer $AGLEDGER_KEY" \
  "$AGLEDGER_URL/v1/records/019e623f-ad7f-7d0f-a623-67a2df7f1777/attestation" > record.cose

# the public verification key(s) — unauthenticated, always on
curl -s "$AGLEDGER_URL/v1/verification-keys" > keys.json

The endpoint returns application/cose-sequence: one tagged COSE_Sign1 envelope (RFC 9052) over an in-toto statement, Ed25519-signed, per chain entry. A record you just notarized has a single chain entry, so this response is exactly one envelope. Verifying it takes only stock libraries — here, Python with cbor2 and cryptography, neither of them ours. Save this as verify.py:

import sys, json, base64, cbor2
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.exceptions import InvalidSignature

cose = open(sys.argv[1], "rb").read()
keys = {k["keyId"]: k["publicKey"] for k in json.load(open(sys.argv[2]))["data"]}

protected, _unprotected, payload, signature = cbor2.loads(cose).value
kid = cbor2.loads(protected)[4]                       # COSE protected header: key id
kid = kid.hex() if isinstance(kid, (bytes, bytearray)) else kid
pubkey = load_der_public_key(base64.b64decode(keys[kid]))
sig_structure = cbor2.dumps(["Signature1", protected, b"", payload])   # RFC 9052 Sig_structure

statement = cbor2.loads(payload)
summary = statement["predicate"]["payload"]["criteria"]["summary"]
try:
    pubkey.verify(signature, sig_structure)
    print(f"[PASS] signature verifies against published key {kid}")
    print(f"       notarized: {summary!r}")
except InvalidSignature:
    print("[FAIL] signature does not verify"); sys.exit(1)
python3 verify.py record.cose keys.json

You should see:

[PASS] signature verifies against published key 3fecdcafbc1a8b56
       notarized: 'About to reconcile invoice INV-4471 against PO-9921'

That is the whole guarantee in one line: the record is authentic and unaltered, proven against a key you fetched once, with the Server out of the loop.

When verification fails

A failed verification is the system working. Change one byte of the signed payload and the signature no longer matches:

tampered = bytearray(payload); tampered[len(tampered)//2] ^= 0x01
try:
    pubkey.verify(signature, cbor2.dumps(["Signature1", protected, b"", bytes(tampered)]))
    print("[BUG ] tampered payload still verified")
except InvalidSignature:
    print("[PASS] tampered payload rejected")
[PASS] tampered payload rejected

A signature mismatch like this is one of a small set of integrity failure classes the auditor-grade verifier reports (CHAIN_SIGNATURE_INVALID, CHAIN_HASH_MISMATCH, CHAIN_LINK_BROKEN, and others). What each one means, and the full database-independent audit handoff, is in the audit guide.

What is next

Air-gapped

Nothing here depends on our website, Docker Hub, or npm. The two inputs — the record envelope and the public keys — are served by your own Server; verify.py uses only stock cryptography libraries and runs with no network. Save the envelope and the key once and you can verify the record on a disconnected machine indefinitely.


Validated against API v0.25.4 on 2026-05-25 (Developer Edition, Docker Compose install).