Vera — runtime trust layer SDK for AI agents (immutable audit trail, HITL approvals, policy enforcement)
Project description
vera-sdk
Python SDK for Vera, a runtime trust layer for AI agents. Vera gives every agent action an immutable, cryptographically verifiable audit trail; supports human-in-the-loop (HITL) approvals for high-risk actions; and captures policy violations so they're queryable after the fact. Built for teams shipping AI into regulated contexts (EU AI Act, GDPR, NIST AI RMF, HIPAA).
If your agent makes decisions a regulator, auditor, or DPO might one day need to inspect, Vera turns "what did the model do?" from an after-the-fact forensics project into a single SQL query.
Contents
- 5-minute quickstart
- Concepts
- Framework cookbooks
- HIPAA and medtech
- Environment variables
- Command-line interface
- Verification
- Troubleshooting
- HTTP API reference
- Versioning policy
- Links
5-minute quickstart
The fastest path from pip install to a record visible in the dashboard.
1. Sign up and create an API key
Visit https://usevera.xyz/register, create an org, and copy the API key from
the API Keys tab in the dashboard. Keys are prefixed al_live_ for prod
and al_test_ for sandbox.
2. Install
pip install vera-sdk
Add framework extras if you want auto-audit integrations (see Framework cookbooks):
pip install "vera-sdk[openai,anthropic,langchain,crewai]"
3. Initialize and audit your first action
import vera
vera.init(api_key="al_live_...", agent_name="loan-screener")
@vera.audit(action_name="approve_loan")
def approve_loan(applicant_id: str, amount: int) -> dict:
return {"approved": True, "score": 0.93}
approve_loan("user_42", 25_000)
That's it. The decorator captures inputs, outcome, timing, and any exception traceback, then ships the record to Vera in the background.
4. Verify from the command line
vera ping
# Connecting to https://api.usevera.xyz...
# OK Authenticated to https://api.usevera.xyz in 142.0ms
# status: ok
# chain_length: 1
vera tail --limit 5
# [ 42] 2026-05-13T07:00:00Z loan-screener approve_loan success
5. See it in the dashboard
Open https://app.usevera.xyz/actions. Your record appears within a few seconds of the call returning. Click into it for the redacted input, outcome, duration, and the signed hash that anchors it to the audit chain.
If you want to try the SDK before signing up, set VERA_DEV=1 and skip the
API key entirely. See Dev mode.
Concepts
A short tour of the abstractions you will actually touch.
vera.init()
One-call setup. Reads explicit kwargs first, falls back to VERA_* env vars,
falls back to defaults. Returns the client and registers it as the default
for every @audit decorator in the process.
vera.init(
api_key="al_live_...",
agent_name="loan-screener",
agent_version="2026.05.13",
framework="openai",
model_id="gpt-4o",
)
Calling init() twice replaces the previous default and closes the old
client. For async codepaths use vera.init_async(...) (synchronous entry
point — drops in-flight records on re-init) at startup, or
vera.init_async_awaitable(...) (async; awaits the previous client's drain
on re-init) inside long-running uvicorn workers where mid-process re-init is
possible and losing queued records is unacceptable.
@vera.audit decorator
Drop-in wrapper for any function. Captures args, kwargs, return value, duration, and exceptions. Failures inside Vera (network, auth, redactor) never mask your function's own exceptions and never add blocking latency to customer code: every record goes through the async background queue.
@vera.audit(action_name="approve_loan", action_type="decision")
def approve_loan(applicant_id: str, amount: int) -> dict:
...
For async functions, use @vera.async_audit(...) and vera.init_async(...).
Redactor
Scrubs PII and secrets out of inputs and outputs before they hit the audit DB. Three layers compose, in order of precedence:
block_keys: case-insensitive kwarg names that are always replaced wholesale. Defaults coverpassword,api_key,ssn,credit_card, and similar.Schema(optional, recommended for regulated data): per-field policyPASSTHROUGH,REDACT, orPATTERNwith a named regex. Unmapped fields fall through tounmapped_policy(deny-by-default).- Regex pass: runs every named pattern over string leaves. Defaults cover SSN, Luhn-checked credit cards, emails, phones, AWS access keys, bearer tokens, JWTs.
Schema mode is what makes Vera safe for regulated workloads. See HIPAA and medtech for the medtech variant.
Dev mode
Run without an API key. Records print to stderr as [vera-dev] {...}.
export VERA_DEV=1
python my_agent.py
Or programmatically:
vera.init(dev=True, agent_name="my-agent")
Useful for tutorials, local iteration, and CI smoke tests where you don't want to provision a real key.
Durable spool
Encrypted, on-disk SQLite buffer that catches records when the in-memory queue is full or the network is down. Survives crashes and process restarts.
import os
os.environ["VERA_SPOOL_PATH"] = "/var/vera/spool.db"
os.environ["VERA_SPOOL_KEY"] = os.environ["VAULT_VERA_SPOOL_KEY"]
vera.init(api_key="al_live_...", agent_name="my-agent")
The spool file is created with mode 0600. Payloads are encrypted at rest
with AES-256-GCM; the key is derived from VERA_SPOOL_KEY via PBKDF2-HMAC-SHA256
(200,000 iterations). If VERA_SPOOL_KEY is unset, the spool refuses to
start: a deliberate guard against accidentally writing plaintext PHI to disk.
See docs/spool-key-rotation.md for the
rotation flow.
Install the optional extra to enable: pip install "vera-sdk[spool]".
Branded errors
Every transport, auth, validation, and rate-limit failure raises a typed
subclass of vera.VeraError so customer code can match on the specific case
instead of parsing exception messages.
from vera import VeraAuthError, VeraRateLimitError, VeraTimeoutError
try:
client.record_action(...)
except VeraAuthError:
# Bad API key, expired key, or revoked.
...
except VeraRateLimitError:
# Backoff and retry.
...
except VeraTimeoutError:
# Network slow, or API key fine but server slow.
...
Full hierarchy: VeraError is the base; subclasses are VeraAuthError,
VeraRateLimitError, VeraServerError, VeraTimeoutError,
VeraNetworkError, and VeraValidationError.
Framework cookbooks
Copy-paste-runnable patterns for the four most common agent frameworks. Each cookbook has a minimal version (just the integration) and a realistic version (integration plus redaction plus durable spool).
OpenAI
Auto-audits every chat.completions.create call: prompts, response, model,
token usage, duration.
Minimal:
import openai
import vera
from vera.integrations.openai import AuditedOpenAI
vera.init(api_key="al_live_...", agent_name="support-bot")
client = AuditedOpenAI(openai.OpenAI(), ledger_client=vera.get_client())
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a customer support agent."},
{"role": "user", "content": "When does my order arrive?"},
],
)
print(response.choices[0].message.content)
Realistic (PHI redaction, durable spool, custom block keys):
import os
import openai
import vera
from vera.integrations.openai import AuditedOpenAI
from vera.redaction import Redactor
# Spool config: encrypted on-disk buffer for outages.
os.environ.setdefault("VERA_SPOOL_PATH", "/var/lib/vera/spool.db")
# VERA_SPOOL_KEY must be set in the environment from your secret manager.
# Build the redactor once. AuditedOpenAI does NOT inherit the redactor
# passed to vera.init() — it must be passed explicitly to the wrapper.
redactor = Redactor.medtech(
extra_block_keys={"insurance_member_id"},
)
vera.init(
api_key=os.environ["VERA_API_KEY"],
agent_name="medical-scribe",
framework="openai",
model_id="gpt-4o",
redactor=redactor,
)
client = AuditedOpenAI(
openai.OpenAI(),
ledger_client=vera.get_client(),
redactor=redactor,
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "Summarise this clinical note."},
{"role": "user", "content": "Patient John Doe, MRN-12345678..."},
],
)
What the audit record looks like:
{
"id": 4271,
"action_name": "openai.chat.completions.create",
"action_type": "llm_call",
"agent_name": "medical-scribe",
"model_id": "gpt-4o",
"result": "success",
"duration_ms": 1842,
"input_data": {
"messages": [
{"role": "system", "content": "Summarise this clinical note."},
{"role": "user", "content": "Patient [REDACTED], [REDACTED]..."}
]
},
"outcome": {
"response": "Patient presents with...",
"usage": {"prompt_tokens": 142, "completion_tokens": 218}
},
"created_at": "2026-05-13T07:00:00Z"
}
Streaming is supported out of the box. The wrapper forwards every chunk to your caller as it arrives, accumulates content in a 1MB-capped buffer, and emits one audit record on stream close.
with client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "..."}],
stream=True,
) as stream:
for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")
Anthropic
Wraps anthropic.Anthropic.messages.create and .messages.stream. Same
audit shape as the OpenAI integration.
Minimal:
import anthropic
import vera
from vera.integrations.anthropic import AuditedAnthropic
vera.init(api_key="al_live_...", agent_name="research-assistant")
client = AuditedAnthropic(anthropic.Anthropic(), ledger_client=vera.get_client())
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{"role": "user", "content": "Summarise the attached policy document."},
],
)
print(response.content[0].text)
Realistic (PHI redaction, spool, explicit redactor):
import os
import anthropic
import vera
from vera.integrations.anthropic import AuditedAnthropic
from vera.redaction import Redactor
vera.init(
api_key=os.environ["VERA_API_KEY"],
agent_name="clinical-summariser",
framework="anthropic",
model_id="claude-sonnet-4-6",
persistent_buffer_path="/var/lib/vera/spool.db",
)
redactor = Redactor.medtech()
client = AuditedAnthropic(
anthropic.Anthropic(),
ledger_client=vera.get_client(),
redactor=redactor,
)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[
{"role": "user", "content": "Generate a discharge summary for MRN-72839102..."},
],
)
Both messages.create(stream=True) and messages.stream(...) paths are
audited as a single record on stream close.
LangChain
Drop the callback handler into any LangChain runnable. Every LLM call, chain
step, tool call, and agent action becomes its own audit record, linked by
run_id so you can reconstruct the full trace in the dashboard.
Minimal:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
import vera
from vera.integrations.langchain import VeraCallbackHandler
vera.init(api_key="al_live_...", agent_name="rag-pipeline")
handler = VeraCallbackHandler(client=vera.get_client())
llm = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_template("Answer concisely: {question}")
chain = prompt | llm
result = chain.invoke(
{"question": "What is the GDPR right to erasure?"},
config={"callbacks": [handler]},
)
print(result.content)
Realistic (agent with tools, PHI redactor, attached at the agent level so
every nested call inherits it). The agent constructor (create_tool_calling_agent)
requires langchain>=0.1.17; the SDK extras only install
langchain-core. Install both:
pip install "vera-sdk[langchain]" "langchain>=0.1.17"
import os
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
import vera
from vera.integrations.langchain import VeraCallbackHandler
from vera.redaction import Redactor
vera.init(
api_key=os.environ["VERA_API_KEY"],
agent_name="patient-intake",
framework="langchain",
)
@tool
def lookup_patient(patient_id: str) -> dict:
"""Fetch a patient record from the EHR."""
return {"patient_id": patient_id, "status": "active"}
handler = VeraCallbackHandler(
client=vera.get_client(),
redactor=Redactor.medtech(),
)
llm = ChatOpenAI(model="gpt-4o")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a clinical intake agent."),
("user", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, [lookup_patient], prompt)
executor = AgentExecutor(agent=agent, tools=[lookup_patient])
executor.invoke(
{"input": "Look up patient_id pat_abc123 and summarise."},
config={"callbacks": [handler]},
)
What gets recorded: one record per LLM call (action_type="llm_call"),
one per chain (action_type="chain"), one per tool invocation
(action_type="tool_call"), one per agent action. Each carries the same
run_id so the dashboard can render the trace as a tree.
CrewAI
Patches crewai.tools.BaseTool._run so every tool call by every agent is
audited automatically. No code changes inside your crew definition.
Minimal:
from crewai import Agent, Crew, Task
from crewai.tools import BaseTool
import vera
from vera.integrations.crewai import enable_crewai_auditing
vera.init(api_key="al_live_...", agent_name="research-crew")
enable_crewai_auditing(client=vera.get_client())
class SearchTool(BaseTool):
name: str = "search"
description: str = "Search the web for a query."
def _run(self, query: str) -> str:
return f"Top result for '{query}'"
researcher = Agent(
role="Researcher",
goal="Find authoritative sources",
backstory="...",
tools=[SearchTool()],
)
task = Task(description="Research the EU AI Act", agent=researcher)
Crew(agents=[researcher], tasks=[task]).kickoff()
Realistic (multi-agent crew with PHI redaction):
import os
from crewai import Agent, Crew, Task
from crewai.tools import BaseTool
import vera
from vera.integrations.crewai import enable_crewai_auditing
from vera.redaction import Redactor
vera.init(
api_key=os.environ["VERA_API_KEY"],
agent_name="medical-research-crew",
framework="crewai",
persistent_buffer_path="/var/lib/vera/spool.db",
)
enable_crewai_auditing(
client=vera.get_client(),
redactor=Redactor.medtech(),
)
# ... rest of your crew, unmodified
Disable with from vera.integrations.crewai import disable_crewai_auditing; disable_crewai_auditing(). Useful for tests.
HIPAA and medtech
⚠️ Using
Redactor.medtech()is one piece of HIPAA-grade handling. It does not make your deployment HIPAA-compliant on its own. You still need a signed BAA with Vera and your other vendors, encryption at rest, access controls, audit-log integrity monitoring, and BCP/DR — none of which are SDK concerns.
Vera ships first-class support for HIPAA-aware workloads. The path:
- Sign a Business Associate Agreement (BAA) with Vera. Required before sending any PHI through the SDK. https://usevera.xyz/baa
- Configure the medtech redactor and durable spool (next section).
- Review your function signatures against the 18 PHI Safe Harbor identifiers and the starter schema.
The medtech redactor
Redactor.medtech() returns a Redactor preconfigured for HIPAA Safe Harbor:
- Starter schema with deny-by-default for unmapped fields. Covers
patient_name,dob,mrn,ssn, address fields, phone, email, free-text fields (notes,description,summary,transcript, and related), and common FHIR Bundle/Resource container keys. - Medtech regex pass: MRN (
MRN-12345678), DOB (multiple formats), IPv4/IPv6, plus URL-with-PHI replacement, on top of the standard SSN, Luhn-checked CC, email, phone, AWS key, JWT, bearer-token patterns. - Extra block_keys covering Safe Harbor categories where a flat-key lookup is sufficient (names, address parts, phone, email, insurance IDs, biometric identifiers, photo URLs, and related).
- BAA reminder log on first use (one INFO log per process, ID
vera-baa-001).
from vera.redaction import Redactor
redactor = Redactor.medtech()
vera.init(api_key="al_live_...", agent_name="my-agent", redactor=redactor)
The 18 PHI Safe Harbor identifiers
| Category | Default coverage |
|---|---|
| Names | block_keys: patient_name, first_name, last_name, full_name; schema REDACT |
| Geographic subdivisions | block_keys: address, street, city, state, zip, postal_code; schema REDACT |
| Dates (DOB, admission, discharge, death) | regex: dob pattern (MM/DD/YYYY, ISO, DD-Mon-YYYY); schema PATTERN on dob and date_of_birth |
| Phone numbers | regex: phone pattern; block_keys: phone, phone_number, mobile, cell |
| Fax numbers | block_keys: fax, fax_number |
| Email addresses | regex: email pattern; block_keys: email, email_address |
| Social Security numbers | regex: ssn pattern; block_keys: ssn, social_security |
| Medical record numbers | regex: mrn pattern; block_keys: mrn, medical_record_number; schema PATTERN |
| Health plan beneficiary numbers | block_keys: insurance_id, policy_number, member_id, subscriber_id |
| Account numbers | customer declares via schema |
| Certificate or license numbers | customer declares via schema |
| Vehicle identifiers (VIN, plate) | customer declares via schema |
| Device identifiers and serial numbers | block_keys: device_id, serial |
| Web URLs containing PHI | regex: url_phi pattern (URLs with embedded MRN, SSN, DOB, or patient_id=) |
| IP addresses | regex: ipv4, ipv6; block_keys: ip, ip_address |
| Biometric identifiers | block_keys: biometric, fingerprint, face_photo |
| Full-face photographs | block_keys: photo, image_url, face_photo |
| Other unique identifiers | covered by deny-by-default schema for unmapped fields |
"Customer declares" means: pass extra_block_keys={"account_number"} to
Redactor.medtech() or add the field to a custom schema. The deny-by-default
unmapped policy means unknown fields are redacted wholesale anyway; declaring
them is for clarity, not safety.
Opaque patient IDs vs MRN-shaped IDs
Use opaque, randomly-generated patient_id values (e.g. pat_a8f3b2c1).
Opaque IDs are HIPAA-safe and the starter schema marks patient_id as
PASSTHROUGH so audit records stay searchable.
If your patient_id is an MRN or otherwise contains real PHI, force
redaction:
redactor = Redactor.medtech(extra_block_keys={"patient_id"})
Same pattern for any field your domain models as ID-shaped but populates with real-world identifiers.
Free-text PHI
⚠️ Use opaque, randomly-generated identifiers for patients, encounters, and clinicians. Free-text fields (notes, descriptions, prose) cannot be reliably scrubbed by regex — names, dates, and identifiers slip through any pattern pass. Declare every prose field that may contain PHI in your
Redactorschema, or omit it from audit payloads entirely.
The starter schema redacts notes, description, summary, comment,
comments, message, transcript, audio_transcript, email_body,
body, and text wholesale.
If you have a free-text field with PHI that uses a non-default name, declare
it explicitly. Note that Schema builds its case-insensitive lookup table at
construction time, so mutating schema.fields AFTER construction does not
take effect. Build the merged schema up front and pass it to
Redactor.medtech(schema=...):
from vera.redaction import Redactor, Schema, FieldRule, FieldPolicy
base = Redactor.medtech_starter_schema()
schema = Schema(
fields={
**base.fields,
"physician_notes": FieldRule(
FieldPolicy.REDACT,
description="Free-text physician notes.",
),
},
unmapped_policy=base.unmapped_policy,
)
redactor = Redactor.medtech(schema=schema)
Custom block keys and patterns
import re
from vera.redaction import Redactor
redactor = Redactor.medtech(
extra_block_keys={"member_id", "external_account_ref"},
extra_patterns=[
("internal_case_id", re.compile(r"\bCASE-\d{6}\b")),
],
)
extra_patterns are added to the default regex pass and can be referenced
by name from schema PATTERN rules.
Durable spool with encryption at rest
Required for HIPAA. Never write plaintext PHI to disk.
export VERA_SPOOL_PATH=/var/lib/vera/spool.db
export VERA_SPOOL_KEY="$(vault read -field=key secret/vera/spool)"
The spool refuses to start without VERA_SPOOL_KEY. AES-256-GCM,
PBKDF2-HMAC-SHA256 with 200,000 iterations, file mode 0600. Rotation: see
docs/spool-key-rotation.md.
Environment variables
Every VERA_* variable the SDK reads. Explicit vera.init(...) kwargs
always win; env vars are the fallback.
| Variable | Default | Description |
|---|---|---|
VERA_API_KEY |
(none) | API key from the dashboard. Prefixed al_live_ (prod) or al_test_ (sandbox). |
VERA_API_URL |
https://api.usevera.xyz |
Backend URL. Override for self-hosted deployments or sandbox. |
VERA_AGENT_NAME |
default-agent |
Identifies this agent in records. |
VERA_AGENT_VERSION |
(none) | Optional agent version string (e.g. 2026.05.13 or a git SHA). |
VERA_MODEL_ID |
(none) | Optional LLM model identifier (e.g. gpt-4o, claude-sonnet-4-6). |
VERA_FRAMEWORK |
(none) | Optional framework name: openai, anthropic, langchain, crewai. |
VERA_DEV |
(unset) | Set to 1 to enable dev mode. Records print to stderr; no API key required. |
VERA_DEV_CONFIRM |
(unset) | Bypass the production-key guard when running in dev mode. Set to 1 only if you really need VERA_DEV=1 with a production-shaped API key (al_live_*). Without this, dev mode refuses to start with such keys to prevent accidentally silencing real audit logging in production. |
VERA_SPOOL_PATH |
(none) | Path to the durable encrypted SQLite spool. Requires VERA_SPOOL_KEY. |
VERA_SPOOL_KEY |
(none) | AES-256-GCM passphrase for the durable spool. Mandatory when VERA_SPOOL_PATH is set. |
Verify resolution at runtime:
vera config show
Command-line interface
The vera CLI ships with the SDK as a runtime entry point. No extra install
step required.
vera init
Scaffold a local .env and walk through API-key creation. Opens the
dashboard's API Keys page in your default browser, then asks you to paste
the new key back into the terminal (key input does not echo). Existing
config (.env in the target path or a VERA_API_KEY env var) is
detected — without --force, init exits 0 with a friendly summary
rather than clobbering.
vera init # interactive — opens browser
vera init --key al_test_xxxxxxxx # script-friendly — skips browser
vera init --force # overwrite existing .env
vera init --env-file path/to/.env # custom path
vera init --dashboard-url https://staging.app.usevera.xyz
Headless detection (SSH_CONNECTION, CI, no DISPLAY on Linux) falls
back to printing the URL instead of opening a browser. The file is
written mode 0600 so the API key isn't world-readable.
vera quickstart
The 5-minute zero-to-record path. Generates a small demo file with two
@vera.gate-decorated actions, runs it against the configured backend so
the auto-discovery pipeline registers the agent, and opens the dashboard
at /customers/<tenant> so you can see your first records.
vera quickstart # default demo file + random tenant
vera quickstart --tenant my_demo # explicit tenant
vera quickstart --no-open # don't auto-open the dashboard (CI)
vera quickstart --non-interactive # fail if no config (don't drop into init)
vera quickstart --skip-run # generate the file but don't execute
If no config is found and the run is interactive, quickstart invokes
vera init first so the whole flow is one command.
vera doctor
Diagnostic checks against your environment. Independent checks for config, connectivity, auth, tenant resolver, spool, SDK version, and codemod availability. Exit code 1 if any check FAILs; WARN / INFO are non-fatal.
vera doctor
# [PASS] config API key present (test), API URL configured
# [PASS] connectivity 200 from https://api.usevera.xyz/health (87ms)
# [PASS] auth authenticated as org org_1234…cdef
# [INFO] tenant no tenant configured — set VERA_TENANT_ID...
# [INFO] spool spool not configured (VERA_SPOOL_PATH unset)
# [PASS] sdk_version vera-sdk 1.0.0
# [PASS] codemod libcst available (codemod feature ready)
#
# summary: 5 pass, 0 fail, 0 warn, 2 info
vera doctor --json # structured output for CI:
# {"checks": [{"name": "config", "status": "PASS", ...}], "summary": {...}}
Out of scope for the current doctor: middleware-wiring detection (needs
a running app context) and customer-webhook URL reachability (the SDK
doesn't know what the customer's webhook URL is). Both are deferred to a
v2 doctor.
vera review-status <review_id>
Fetch the status of a single approval. Three modes: one-shot lookup,
machine-readable JSON, and --watch polling.
# One-shot human-readable summary.
vera review-status app_01H7XKCRJF8
# Review app_01H7XKCRJF8 [… PENDING] risk: high
#
# agent dpo-bot
# action delete_customer_record
# summary Hard-delete user record per GDPR Art. 17
# subject user_42
# requested 2026-05-24 14:27:00 UTC (3m ago)
# expires 2026-05-24 14:37:00 UTC (in 7m)
# approvers 2 required
#
# Context:
# rows 1
# table customers
# Raw ApprovalResponse JSON (pretty-printed) — pipe to jq.
vera review-status app_01H7XKCRJF8 --json | jq .status
# Watch until the approval resolves (or you Ctrl-C).
vera review-status app_01H7XKCRJF8 --watch --interval 3
# Watch with a 5-minute deadline; exit code 3 if it never resolves.
vera review-status app_01H7XKCRJF8 --watch --timeout 300
# NDJSON for streaming pipelines (one object per poll).
vera review-status app_01H7XKCRJF8 --watch --json \
| jq -c 'select(.status != "pending")'
Times are always rendered in UTC; relative phrases ("3m ago", "in 7m")
are computed from your local clock. Colors auto-disable when stdout is
not a TTY, when NO_COLOR is set, or with --no-color.
Exit codes:
0: approval fetched (or reached a terminal state under--watch).1: approval not found (404).2: auth failure, wrong-tier key, network error, rate limit, or server error (specific reason printed to stderr).3:--watch --timeoutreached before the approval resolved.130: user pressed Ctrl-C.
Compliance-pipeline pattern:
vera review-status "$ID" --watch --timeout 600
case $? in
0) echo "approved or rejected — proceed" ;;
3) echo "still pending after 10m — escalate" ;;
*) echo "transport or auth failure — abort" ;;
esac
vera config show
Print the effective configuration: env vars and computed defaults.
vera config show
# api_url https://api.usevera.xyz
# api_key ...x9f2
# agent_name loan-screener
# agent_version (unset)
# model_id gpt-4o
# framework openai
# persistent_buffer_path (off)
# dev_mode off
The API key is masked by default (...<last4> for keys at least 12 chars,
*** otherwise). Pass --reveal-secrets to print it in full. Useful when
debugging a wrapper script:
vera config show --reveal-secrets
vera ping
Verify the API key authenticates against /v1/verify. Reports latency.
vera ping
# Connecting to https://api.usevera.xyz...
# OK Authenticated to https://api.usevera.xyz in 142.0ms
# status: ok
# chain_length: 14271
Exit codes:
0: authenticated successfully.1: auth failed, timeout, network error, or server error (specific reason printed to stderr).2: unexpected exception (please file an issue).
CI smoke-test pattern:
vera ping && echo "Vera reachable" || exit 1
vera tail
Tail recent records for the org.
vera tail --limit 20
# Filter by agent.
vera tail --agent loan-screener
# Filter by result.
vera tail --result failure
# Follow new records as they arrive.
vera tail --follow --interval 2
# Emit JSON Lines for pipelines.
vera tail --json --limit 100 | jq 'select(.duration_ms > 5000)'
# Quick check: count failed actions in the last batch.
vera tail --result failure --json --limit 500 | jq -s 'length'
All commands respect every VERA_* env var. Run vera <command> --help for
the full option set.
Verification
Vera's value to a regulator or auditor depends on three guarantees being verifiable independently of Vera itself:
- The record you're shown was the record we captured (Merkle inclusion).
- The checkpoint that sealed it has not been altered (KMS signature).
- The chain of records leading up to it is intact (previous-hash continuity).
Three CLI commands cover the verification flow end-to-end. They share a common bundle format so an auditor can verify a customer's entire evidence trail with only the bundle file and (for HMAC-signed bundles) a shared secret.
A runnable, no-credentials walkthrough lives in
examples/verify_offline_walkthrough/.
Run it once before reading the rest of this section — the shape lands
faster from the bundle than from prose.
vera verify --merkle-proof <record_id>
Online verification. Hits Vera, downloads the Merkle proof for one record, then validates it locally against the customer's KMS public-key history. Use this for spot checks during incident review or before filing a regulator response.
vera verify --merkle-proof act_01H7XKCRJF8
# OK action_record act_01H7XKCRJF8 verified against checkpoint cp_2026-05-12
# merkle_root: 93276b21b8623b35...
# kms_key_id: vera-prod-2026q2
# signed_at: 2026-05-12T23:59:59+00:00
Exit codes:
0: proof valid against the live KMS key history.1: proof invalid (tampered leaf, tampered sibling, root mismatch, or signature invalid). The specific reason prints to stderr.2: network / auth / 404 / 409 (record found but its checkpoint hasn't sealed yet — retry afterRetry-After: 60per Wave 3B.2).
vera evidence-export --customer <tenant_id> --since <date> --out <path>
Build a self-contained tar.gz bundle of one customer's records over a date range. The bundle contains the Merkle proofs, signed checkpoints, and KMS key history needed to verify every record without further network calls.
vera evidence-export --customer cleveland_clinic \
--since 2026-02-25 --until 2026-05-25 \
--out ./evidence-cleveland-2026q2.tar.gz
Selective disclosure is structural: the bundle includes only Cleveland Clinic's records and only the sibling hashes their Merkle proofs require. An auditor inspecting the bundle cannot count or reconstruct records belonging to any other customer.
For HMAC-signed bundles, hand the shared secret over an out-of-band channel (BAA-covered email, signed envelope, in-person handoff). For asymmetric bundles, the public keys travel inside the bundle and the auditor needs nothing else.
Tail-window skips. Records written close to the moment you run
evidence-export may be enumerated by the action list but not yet
sealed into a checkpoint (HTTP 409 checkpoint_pending); records
deleted server-side after enumeration return 404 record_not_found.
Both are skipped — the export does not fail — but the omission is
surfaced two ways so it stays auditable: a structured WARN block
lists each skipped record ID and reason to stderr, and the same list
is written to manifest.json under the skipped_records field (an
array of {id, reason} objects; empty on a clean run). To recover a
tail-window skip, re-run --since one checkpoint-cadence period
earlier once the tail has sealed.
vera verify --offline <bundle_path>
Verify a bundle with no network access. The auditor runs this; you don't need to.
vera verify --offline ./evidence-cleveland-2026q2.tar.gz
# OK 1,247 record(s) verified across 90 checkpoint(s)
# OK Chain integrity: all sibling links validate
# OK KMS signatures: all valid (vera-prod-2026q2)
Exit codes:
0: every record verifies, every checkpoint signature validates.1: verification failed. The first failure prints to stderr with a structured reason:merkle_proof_invalid,signature_invalid,root_mismatch,kms_key_not_in_history,record_count_mismatch,manifest_missing, orhmac_secret_missing.2: bundle malformed or unreadable (bad tar, missing files).
End-to-end session
Copy-paste-runnable. Substitute your real tenant_id and date range.
# 1. Export evidence for a specific customer over the last 90 days.
vera evidence-export --customer cleveland_clinic \
--since 2026-02-25 --until 2026-05-25 \
--out ./evidence-cleveland-2026q2.tar.gz
# 2. Hand the bundle to your compliance team or auditor.
scp ./evidence-cleveland-2026q2.tar.gz auditor@example.com:~
# 3. The auditor verifies offline — no Vera credentials needed.
vera verify --offline ./evidence-cleveland-2026q2.tar.gz
# OK 1,247 record(s) verified across 90 checkpoint(s)
# OK Chain integrity: all sibling links validate
# OK KMS signatures: all valid (vera-prod-2026q2)
For HMAC-signed bundles, the auditor needs the secret in their environment before step 3:
export VERA_HMAC_SECRET="$(vault read -field=secret secret/vera/hmac/2026q2)"
vera verify --offline ./evidence-cleveland-2026q2.tar.gz
Bundle shape
What vera evidence-export writes and what vera verify --offline
reads. Field names match the live API exactly so an auditor familiar
with one is immediately at home with the other.
bundle.tar.gz
manifest.json bundle metadata + record + checkpoint counts
checkpoints/<checkpoint_id>.json one per sealed checkpoint
records/<record_id>.json one Merkle proof payload per record
kms_keys.json KMS key history (algorithm + PEM per key_id)
manifest.json — top-level summary. Fields:
| Field | Type | Description |
|---|---|---|
schema_version |
int | Bundle schema version. Currently 1. |
tenant_id |
string | The customer this bundle is scoped to. |
org_id |
string | The Vera organization that produced the bundle. |
since / until |
ISO date | The window covered. |
generated_at |
ISO timestamp | When the bundle was produced. |
record_count |
int | Total records included. |
checkpoints |
array | One summary entry per sealed checkpoint (id, date, record_count, merkle_root). |
kms_algorithm |
string | Dominant signing algorithm. hmac-sha256 (LocalKMS), kms-hmac-sha256 (AWS KMS HMAC), rsa-pss-sha256, or ecdsa-p256-sha256. |
hmac_secret_required |
bool | true if any checkpoint in the bundle is HMAC-signed. |
skipped_records |
array | Records enumerated but not included in the bundle, with reason. Each entry is {id, reason}; reason is checkpoint_pending (HTTP 409 — tail-window race) or record_not_found (HTTP 404 — deleted server-side after enumeration). Empty array on a clean run; same list also printed to stderr at export time. |
checkpoints/<id>.json — one per sealed checkpoint. Field names
match the GET /v1/checkpoints/{date} response body (Wave 3B.1), with
two additions the bundle requires (date for window grouping;
algorithm so an HMAC-vs-asymmetric bundle is self-describing without
a kms_keys.json cross-lookup):
| Field | Type | Description |
|---|---|---|
checkpoint_id |
string | Stable checkpoint identifier. |
org_id |
string | Owning organization. |
date |
ISO date | The day this checkpoint sealed. Bundle-only — not on the live endpoint. |
sequence_at_checkpoint |
int | Last record sequence number sealed under this checkpoint. |
hash_at_checkpoint |
hex | Chain head hash at seal time. |
merkle_root |
hex | Merkle root over the records in this checkpoint window. |
signed_at |
ISO timestamp | KMS signing timestamp. |
kms_key_id |
string | KMS key that signed this checkpoint. Look up in kms_keys.json. |
algorithm |
string | One of hmac-sha256, kms-hmac-sha256, rsa-pss-sha256, ecdsa-p256-sha256. Bundle-only — the live endpoint omits this since the current key's algorithm is fetched from /v1/kms/keys (Wave 3A.a). |
signature |
hex | KMS signature over the canonical message bytes. |
record_count |
int | Records sealed under this checkpoint. |
head_action_id |
string | null | Last action record id sealed under this checkpoint (passthrough from the live endpoint). |
prior_checkpoint_id |
string | null | The previous checkpoint's id, or null for the first checkpoint (passthrough from the live endpoint). |
records/<record_id>.json — one Merkle proof per record. Mirrors
the GET /v1/records/{id}/merkle-proof response body verbatim
(Wave 3B.2):
| Field | Type | Description |
|---|---|---|
action_record_id |
string | The record this proof is for. |
action_record_canonical |
string | Canonical JSON bytes that hashed into the chain at insert time. |
previous_hash |
string | Predecessor leaf in the per-org hash chain. The literal sentinel "GENESIS" for the first record in a chain (not an empty string), otherwise the prior record's hex leaf_hash. Required so an offline verifier can enforce the chain rule sha256(previous_hash + canonical) == leaf_hash; without it the verifier can only prove Merkle path inclusion. |
leaf_hash |
hex | SHA-256(previous_hash + action_record_canonical). |
merkle_path |
array | Sibling hashes from leaf to root: [{sibling_hash, direction}, ...]. direction is "left" or "right". |
merkle_root |
hex | The root of the Merkle tree for this checkpoint window. |
checkpoint_id |
string | The checkpoint that sealed this record. |
checkpoint_signed_at |
ISO timestamp | When that checkpoint was signed. |
kms_key_id |
string | KMS key used. Look up in kms_keys.json. |
kms_signature |
hex | The checkpoint signature. |
kms_algorithm |
string | The signing algorithm. |
kms_public_key_pem |
string | null | The PEM for asymmetric keys; null for HMAC. |
kms_keys.json — array of every KMS key referenced by any
checkpoint in the bundle. The verifier walks this when a checkpoint's
key_id doesn't match the current production key (i.e., a rotation
happened during the bundle's window):
| Field | Type | Description |
|---|---|---|
key_id |
string | Unique identifier. |
algorithm |
string | hmac-sha256, kms-hmac-sha256, rsa-pss-sha256, or ecdsa-p256-sha256. |
public_key_pem |
string | null | PEM-encoded public key for asymmetric algorithms; null for HMAC. |
first_seen_at |
ISO timestamp | When this key first signed a checkpoint. |
retired_at |
ISO timestamp | null | When this key was rotated out, if at all. |
HMAC vs asymmetric KMS
The signing material is the load-bearing distinction between a self-contained bundle and one that needs out-of-band coordination.
| Mode | Bundle self-contained? | What the auditor needs |
|---|---|---|
hmac-sha256 (LocalKMS) |
No | The bundle + VERA_HMAC_SECRET shared out-of-band |
kms-hmac-sha256 (AWS KMS HMAC) |
No | The bundle + VERA_HMAC_SECRET shared out-of-band |
rsa-pss-sha256 |
Yes | Just the bundle |
ecdsa-p256-sha256 |
Yes | Just the bundle |
HMAC is fine for internal review and dev workflows. For external auditors, regulator submissions, and any context where you'd rather not hand a customer's compliance team your signing secret, move the org to asymmetric KMS. Contact support@usevera.xyz to coordinate the migration; the KMS history table (Wave 3A.a) means past checkpoints keep verifying against their original signing keys without re-signing.
Verification-specific environment variables
| Variable | Used by | Description |
|---|---|---|
VERA_HMAC_SECRET |
vera verify --offline |
Shared HMAC secret for HMAC-signed bundles. Required when manifest.json has hmac_secret_required: true. Mirrors the backend's ACTIONLEDGER_SIGNING_KEY value. |
VERA_TARGET_ORG_ID |
vera verify --offline, vera evidence-export |
Pin an org context when your API key is multi-org or when bundle metadata needs explicit scoping. Optional for single-org accounts. |
The standard VERA_API_KEY / VERA_API_URL apply only to
evidence-export and verify --merkle-proof (both hit Vera).
verify --offline never reads them — it has no network surface.
Troubleshooting
VeraAuthError: Authentication failed
- Verify your key with
vera config show: is it set, is it the right one? - Confirm with the live API:
vera ping. - Prod keys are prefixed
al_live_; sandbox keysal_test_. Make sure you aren't pointing prod traffic at the test environment or vice versa.
My records aren't appearing in the dashboard
- Run
vera pingto confirm the network can reachapi.usevera.xyzand the key authenticates. - Check for
[vera-dev]lines on stderr. That meansVERA_DEV=1is set somewhere and records are going to the dev sink, not to Vera. - Check stderr for
[vera] WARNING: no default client configured. That means@vera.auditis running butvera.init()was never called. - Confirm
agent_namematches what you're filtering on in the dashboard. - The async queue flushes on a timer. Records can take a few seconds to
arrive. For tests, use the
vera_sdk_recordingpytest fixture orclient.close()to drain synchronously.
VeraRateLimitError
You're sending records faster than your plan allows. Options:
- Reduce per-call traffic. Make sure you aren't double-wrapping the same
function with
@auditand a framework integration. - Contact support@usevera.xyz to raise your plan limits.
VeraTimeoutError on every call
VeraClient's default HTTP timeout is 5 seconds. If you're on a slow network, raise it:vera.init(..., timeout=15).- The
@auditdecorator pipeline is async. It never blocks on Vera and never raisesVeraTimeoutErrorto your code. Timeouts surface inside the worker thread and end up asresult="failure"records.
VeraValidationError: ...
A call has an invalid field. The exception message names the offending field. Common cases:
record_action(...): missingaction_name, oraction_namelonger than the server limit. The exception message names the field.request_approval(...):risk_tieroutside{low, medium, high, critical}, or adata_subject_idlonger than 255 chars.
Spool refuses to start: "Spool requires a non-empty passphrase"
VERA_SPOOL_PATH is set but VERA_SPOOL_KEY is not. The spool refuses to
write plaintext to disk. Either set VERA_SPOOL_KEY from your secret
manager or unset VERA_SPOOL_PATH.
"Check VERA_SPOOL_KEY: the key supplied does not match..."
The spool file on disk was encrypted with a different VERA_SPOOL_KEY.
Either restore the original key or rotate per the
docs/spool-key-rotation.md flow.
ImportError: openai is required for OpenAI integration
You imported vera.integrations.openai without installing the extra. Fix:
pip install "vera-sdk[openai]"
Same pattern for anthropic, langchain, crewai, and spool.
DeprecationWarning: max_input_length is deprecated
The LangChain callback's max_input_length and max_output_length kwargs
are ignored. Pass redactor=Redactor(max_length=...) instead.
Records are being redacted that shouldn't be
The redactor's block_keys are aggressive by design. Better to over-redact
than leak. If a kwarg is blocked but should pass through, build a custom
Redactor with a narrower block_keys set or declare the field
PASSTHROUGH in a schema. See Concepts above.
Tests are seeing records from another test
Use the vera_sdk_recording pytest fixture. It auto-loads when the SDK is
installed, clears records between tests, and avoids the network entirely.
def test_loan_decision(vera_sdk_recording):
approve_loan(applicant_id="user_42", amount=10000)
assert len(vera_sdk_recording.records) == 1
assert vera_sdk_recording.records[0]["action_name"] == "approve_loan"
HTTP API reference
Full HTTP API docs: https://docs.usevera.xyz/api. The same backend exposes auto-generated OpenAPI at https://api.usevera.xyz/docs (Swagger UI) and https://api.usevera.xyz/openapi.json (machine-readable schema).
Most teams should not need to call the HTTP API directly. The SDK covers every endpoint. Use the raw API when:
- You are integrating from a language other than Python (Go, TypeScript, Ruby). Generate a client from the OpenAPI schema.
- You are integrating from a no-code platform (Zapier, n8n).
- You need an endpoint the SDK does not yet wrap (the SDK trails the API by a release or two for niche endpoints).
Authentication is Authorization: Bearer <api_key> on every request.
Versioning policy
vera-sdk follows Semantic Versioning. Breaking
behavioral changes ship behind a DeprecationWarning for at least one minor
release before the default flips. See CHANGELOG.md for
the release history and MIGRATION.md for upgrade notes.
Links
- Dashboard: https://app.usevera.xyz
- HTTP API docs: https://docs.usevera.xyz/api
- BAA request: https://usevera.xyz/baa
- Changelog:
CHANGELOG.md - Migration notes:
MIGRATION.md - Spool key rotation:
docs/spool-key-rotation.md - Support: support@usevera.xyz
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vera_sdk-1.2.0.tar.gz.
File metadata
- Download URL: vera_sdk-1.2.0.tar.gz
- Upload date:
- Size: 405.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3390064e6f4fd2cb454cabb39d416f0b233eb558ff21c88c35da63bcc38994da
|
|
| MD5 |
40ab041c2547bedadda387800a3dbacc
|
|
| BLAKE2b-256 |
25f90874c0fee3f67f810d8eacd5e83b97f6596cd237e47dbbfbf97ef94bf7b5
|
File details
Details for the file vera_sdk-1.2.0-py3-none-any.whl.
File metadata
- Download URL: vera_sdk-1.2.0-py3-none-any.whl
- Upload date:
- Size: 228.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64afa58db174b4d8de5e9141242ebf372be125b61bae0d712a83ec596b0fcc80
|
|
| MD5 |
85a50715862cec9c1eef18289e77baeb
|
|
| BLAKE2b-256 |
2a7bf18ed5b0ae1134e226ce64720b3cbb77b4f0a451f2d4425705fc789713af
|