> Markdown version of https://agledger.ai/docs/sdk/
> Full index of this site for AI assistants: https://agledger.ai/llms.txt

# 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](/api/) 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
// TypeScript — reach any route the SDK does not yet model
const result = await client.request('POST', '/v1/custom/endpoint', { foo: 'bar' })
```

```python
# Python — same escape hatch
result = client.request("POST", "/v1/custom/endpoint", json={"foo": "bar"})
```

## Install and initialize

```bash
npm install @agledger/sdk@1.9.0
```

```bash
pip install 'agledger==1.9.0'
```

AGLedger is self-hosted, so the client needs your Server's base URL and an API key (see [Authentication](/docs/guides/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.

```typescript
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)
})
```

```python
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.

```typescript
const me = await client.auth.getMe()
console.log(me.role, me.scopes)
```

```python
me = client.auth.get_me()
print(me.role, me.scopes)
```

Python also ships `AsyncAgledgerClient` with the same method surface under `await`:

```python
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.

```typescript
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'
```

```python
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.

```typescript
console.log(record.signedStatement?.chainPosition, record.signedStatement?.leafHash)
```

```python
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.

```typescript
// 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'
```

```python
# 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.

```typescript
await client.records.submitVerdict(record.id, { completionId: completion.id, verdict: 'accept' })
```

```python
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.

```typescript
const next = client.records.getValidTransitions(record) // e.g. ['register', 'activate', ...]
```

```python
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.

```typescript
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
```

```python
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.

```typescript
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}`)
}
```

```python
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.

```typescript
import { readFileSync } from 'node:fs'
import { verifyExport } from '@agledger/sdk/verify'

const exportData = JSON.parse(readFileSync('audit-export.json', 'utf8'))
const result = verifyExport(exportData)
```

```python
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:

```bash
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.

```typescript
const result = verifyExport(exportData, {
  publicKeys: { 'key-2026-q2': '<base64 SPKI DER>' },
  requireKeyId: 'key-2026-q2',
})
```

```python
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.

```typescript
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
  }
}
```

```python
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")
```

{/* TESTBED: confirm Python error attribute names recovery_hint / refresh_url / retry_after on the typed exceptions */}

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](/docs/guides/webhooks/); that page owns webhook mechanics.

---

*Method names, signatures, and the offline-verify behavior on this page are drawn from `@agledger/sdk` v1.9.0 (npm) and `agledger` v1.9.0 (PyPI). Request and response field shapes are owned by the [API reference](/api/); confirm field-level detail there against your own Server.*
