SDK
There are two first-party SDKs - @agledger/sdk for TypeScript and agledger for Python. Both wrap the same REST API thinly: one client object, one resource per route group, one method per call. The method names, parameters, and return shapes track the API one-to-one. This page covers the jobs an agent does most often - initialize a client, notarize a record, run the gated lifecycle, and verify an audit export offline - with the TypeScript and Python call paired for each.
Use the SDK when you are writing an agent in TypeScript or Python and want typed methods, retries, and an offline verifier without hand-rolling HTTP. For request and response field detail, the API reference is canonical - this page does not restate it.
The SDK is a thin wrapper
The SDK does not hold a model of the lifecycle, decide what to do next, or cache state. It signs nothing and verifies nothing on the wire - every call is a single HTTP request, and the response is the API's response unchanged. That is deliberate. The API teaches the caller what to do next inside each response: a created record carries nextActions (the exact transition names valid right now) and completionHint (the evidence fields the next step expects); an error carries code, retryable, suggestion, and - on a state rejection - recoveryHint and refreshUrl naming the corrective call. The guidance lives in the response, so the SDK stays small and never drifts behind the API.
Two consequences follow. First, read the response, not the SDK, to decide the next call - record.nextActions over a hard-coded transition order. Second, when a route is newer than your SDK version, you do not have to wait for a typed method: every client exposes a request escape hatch that forwards a method, path, and body to the API verbatim.
// TypeScript — reach any route the SDK does not yet model
const result = await client.request('POST', '/v1/custom/endpoint', { foo: 'bar' })
# Python — same escape hatch
result = client.request("POST", "/v1/custom/endpoint", json={"foo": "bar"})
Install and initialize
npm install @agledger/[email protected]
pip install 'agledger==1.1.0'
AGLedger is self-hosted, so the client needs your Server's base URL and an API key (see Authentication for minting one). The TypeScript client takes both in its constructor; the Python client reads AGLEDGER_API_KEY from the environment when api_key is omitted and supports a context manager for connection cleanup.
import { AgledgerClient } from '@agledger/sdk'
const client = new AgledgerClient({
apiKey: process.env.AGLEDGER_API_KEY!,
baseUrl: process.env.AGLEDGER_EXTERNAL_URL!, // your Server's URL
// optional: maxRetries (default 3), timeout (ms, default 30_000)
})
import os
from agledger import AgledgerClient
with AgledgerClient(
api_key=os.environ["AGLEDGER_API_KEY"],
base_url=os.environ["AGLEDGER_EXTERNAL_URL"], # your Server's URL
# optional: max_retries, timeout (seconds)
) as client:
...
Confirm the credential resolves before wiring it into an agent. getMe / get_me echoes the resolved identity, role, and scopes - the same data GET /v1/auth/me returns.
const me = await client.auth.getMe()
console.log(me.role, me.scopes)
me = client.auth.get_me()
print(me.role, me.scopes)
Python also ships AsyncAgledgerClient with the same method surface under await:
from agledger import AsyncAgledgerClient
async with AsyncAgledgerClient() as client: # reads AGLEDGER_API_KEY
record = await client.records.get("rec-123")
Notarize a record (the on-ramp)
The common case is a single agent notarizing what it is about to do. For a notarize-only Type - one whose schema declares no completion - records.create is the whole interaction: the record is signed into the chain and terminalizes at RECORDED in that one call. An agent key resolves its own org and principal from the key, so you pass only the Type and the criteria.
const record = await client.records.create({
type: 'NOTARIZE-ACTION-v1',
criteria: {
action: 'rotate-prod-db-credentials',
requested_by: 'ops-oncall',
},
})
console.log(record.id, record.status) // status: 'RECORDED'
record = client.records.create(
type="NOTARIZE-ACTION-v1",
criteria={
"action": "rotate-prod-db-credentials",
"requested_by": "ops-oncall",
},
)
print(record.id, record.status) # status: 'RECORDED'
The returned record carries a signedStatement block - the chain position, leaf hash, previous hash, and the signing key id - so a notarize-only caller can confirm the chain head without a follow-up call.
console.log(record.signedStatement?.chainPosition, record.signedStatement?.leafHash)
print(record.signed_statement.chain_position, record.signed_statement.leaf_hash)
Run the gated lifecycle
A Type that declares a completion schema runs the gated lifecycle: create the record, activate it, the performer submits a Completion as evidence, and a verdict - accept or reject - terminalizes it. The SDK exposes one method per step. createAndActivate / create_and_activate folds the create, register, and activate calls into one helper.
// 1. Principal creates and activates the record
const record = await client.records.createAndActivate({
type: 'DATA-EXPORT-v1',
contractVersion: '1',
platform: 'internal-etl',
performerAgentId: 'agt-123',
criteria: { description: 'Nightly warehouse export', output_format: 'parquet', row_count_min: 500_000 },
})
// 2. Performer submits a Completion (evidence of what was done)
const completion = await client.completions.submit(record.id, {
evidence: { deliverable: '/data/exports/2026-03-10.parquet', deliverable_type: 'file_ref', row_count: 487_231 },
})
// 3. In auto mode the gate evaluates inline and settles; read the verdict off the record
const settled = await client.records.get(record.id)
console.log(settled.status, settled.verdict) // 'FULFILLED', 'accept'
# 1. Principal creates and activates the record
record = client.records.create_and_activate(
type="DATA-EXPORT-v1",
contract_version="1",
platform="internal-etl",
performer_agent_id="agt-123",
criteria={"description": "Nightly warehouse export", "output_format": "parquet", "row_count_min": 500_000},
)
# 2. Performer submits a Completion (evidence of what was done)
completion = client.completions.submit(
record.id,
evidence={"deliverable": "/data/exports/2026-03-10.parquet", "deliverable_type": "file_ref", "row_count": 487_231},
)
# 3. In auto mode the gate evaluates inline and settles; read the verdict off the record
settled = client.records.get(record.id)
print(settled.status, settled.verdict) # 'FULFILLED', 'accept'
When the Type holds for a principal verdict rather than auto-settling, the principal submits it directly. The principal is always the real judge - the SDK only carries the call.
await client.records.submitVerdict(record.id, { completionId: completion.id, verdict: 'accept' })
client.records.submit_verdict(record.id, completion_id=completion.id, verdict="accept")
A record moves through the chain by named transitions, not arbitrary status assignment. Read record.nextActions for the exact set valid right now rather than hard-coding an order; getValidTransitions / get_valid_transitions is a client-side lookup over the same table with no API call.
const next = client.records.getValidTransitions(record) // e.g. ['register', 'activate', ...]
next_actions = client.records.get_valid_transitions(record)
Read the delegation chain
When work is subcontracted, delegate creates a child record linked to its parent, and the chain methods read the linked graph back.
const child = await client.records.delegate(record.id, {
type: 'DATA-EXPORT-v1',
performerAgentId: 'agt-subprocessor',
criteria: { description: 'Transform stage' },
})
const chain = await client.records.getChain(record.id) // RecordRow[], parent through children
child = client.records.delegate(
record.id,
type="DATA-EXPORT-v1",
performer_agent_id="agt-subprocessor",
criteria={"description": "Transform stage"},
)
chain = client.records.get_chain(record.id) # list[RecordRow], parent through children
Export and verify a chain offline
The audit export is a record's full hash-chained, signed trail. Both SDKs ship a standalone verifier that re-walks the chain and checks every Ed25519 signature with no dependency on the Server - the load-bearing property of an offline auditor. Pull the export through the client, then verify it.
import { verifyExport } from '@agledger/sdk/verify'
const exportData = await client.records.getAuditExport(record.id)
const result = verifyExport(exportData)
if (!result.valid) {
console.error(`Broken at position ${result.brokenAt?.position}: ${result.brokenAt?.code}`)
}
from agledger.verify import verify_export
export_data = client.records.get_audit_export(record.id)
result = verify_export(export_data)
if not result.valid:
print(f"Broken at position {result.broken_at.position}: {result.broken_at.code}")
verifyExport / verify_export returns valid plus a per-entry breakdown and a signatureCoverage / signature_coverage discriminator - a contiguous, unbroken chain does not imply every entry was cryptographically signed, so the result reports the two separately. A failure names the first broken position and a machine-readable code from a SCREAMING_SNAKE taxonomy (CHAIN_LINK_BROKEN, CHAIN_HASH_MISMATCH, CHAIN_SIGNATURE_INVALID, and so on).
Air-gapped verification
Neither verifier calls home. Persist the export to disk on the connected side, carry it across the air gap, and verify it where there is no network. Both verifiers read the export from a plain object, so nothing in the verify path depends on the Server, this website, npm, or PyPI once the SDK and its verify dependencies are installed.
import { readFileSync } from 'node:fs'
import { verifyExport } from '@agledger/sdk/verify'
const exportData = JSON.parse(readFileSync('audit-export.json', 'utf8'))
const result = verifyExport(exportData)
import json
from agledger.verify import verify_export
with open("audit-export.json") as f:
export_data = json.load(f)
result = verify_export(export_data)
The Python verifier needs cbor2 and cryptography for COSE_Sign1 decoding and Ed25519 verification - install them with the verify extra:
pip install 'agledger[verify]'
To verify against a trusted out-of-band key set rather than the keys embedded in the export - the high-assurance posture - pass the public keys explicitly, and pin the expected signing key so an export signed by an unexpected key fails.
const result = verifyExport(exportData, {
publicKeys: { 'key-2026-q2': '<base64 SPKI DER>' },
requireKeyId: 'key-2026-q2',
})
result = verify_export(
export_data,
public_keys={"key-2026-q2": "<base64 SPKI DER>"},
require_key_id="key-2026-q2",
)
Handle errors
API errors raise typed exceptions carrying the API's own guidance fields. Branch on the type, and on a state rejection read recoveryHint / refreshUrl to find the corrective call rather than blindly retrying.
import { UnprocessableError, RateLimitError } from '@agledger/sdk'
try {
await client.records.transition(record.id, 'activate')
} catch (err) {
if (err instanceof UnprocessableError) {
console.error(err.recoveryHint, err.refreshUrl) // wrong state — re-fetch and follow the named action
} else if (err instanceof RateLimitError) {
console.error(`retry after ${err.retryAfter}s`)
} else {
throw err
}
}
from agledger import UnprocessableError, RateLimitError
try:
client.records.transition(record.id, "activate")
except UnprocessableError as err:
print(err.recovery_hint, err.refresh_url) # wrong state — re-fetch and follow the named action
except RateLimitError as err:
print(f"retry after {err.retry_after}s")
Both clients retry 429 and 5xx responses with exponential backoff up to maxRetries / max_retries before raising, so the exception you catch is the final outcome, not a transient blip.
Webhooks
When an agent both notarizes and receives the event stream, the SDK ships webhook verifiers under a dedicated entry point - HMAC for receiver-only integrity and Ed25519 (RFC 9421) for non-repudiable Settlement Signals. The verifier methods (verifySignature, verifyRfc9421 in TypeScript; verify_signature, verify_rfc9421 in Python) are documented end to end on the Webhooks guide; that page owns webhook mechanics.
Method names, signatures, and the offline-verify behavior on this page are drawn from @agledger/sdk v1.2.0 (npm) and agledger v1.1.0 (PyPI). Request and response field shapes are owned by the API reference; confirm field-level detail there against your own Server.