# Python SDK

Kind

guide

For

developer

Verified against

veritio 0.0.1 (Python) at veritio@c4100ee · @veritio/core@0.4.7

The Python SDK is a producer, not a verifier. It builds portable audit events, evidence edges, evidence commits, governed action drafts, risk assessments, and `security.risk` assertions, and it hashes all of them with the same canonical bytes TypeScript and Go produce. It does not verify record chains, does not record provenance, does not assemble export bundles, and does not own storage.

That boundary is the most important thing on this page. A Python service can participate fully in a Veritio deployment on the write side. On the read-and-prove side, one of the three chain verifiers exists in Python and two do not.

## What the package contains

[Section titled “What the package contains”](#what-the-package-contains)

The distribution is one module tree with five files, and everything public is re-exported from the package root.

```text
veritio/
  event.py            events, edges, commits, canonical JSON, hashing, redaction
  governed_change.py  entity definitions, change and action drafts, revision ids
  risk.py             signal normalization, per-step scoring, episode rollup, assertions
  risk_policy.py      temperature-derived policies over DEFAULT_RISK_POLICY
  templates.py        26 event builders plus the audit-log classification helpers
```

There is no `store.py`, no `provenance.py`, and no `export_bundle.py`. Their absence is a package boundary, not a protocol gap: the protocol defines those behaviors, and at the verified revision only TypeScript implements them.

## Install the verified revision

[Section titled “Install the verified revision”](#install-the-verified-revision)

The package is `veritio` version `0.0.1` and is installed from the pinned checkout rather than from an index.

Terminal window

```sh
git clone https://github.com/getveritio/veritio.git
git -C veritio checkout c4100ee7b678d0c6b227c67ae6ea1d8a1f373967
python3 -m pip install -e ./veritio/sdks/python
```

`pyproject.toml` declares `requires-python = ">=3.11"`. Confirm which checkout is actually imported:

Terminal window

```sh
python3 -c "import veritio; print(veritio.__file__)"
```

The printed path must resolve inside the checked-out `veritio/sdks/python/src/veritio` directory. If it resolves anywhere else, every hash on this page is being produced by code you have not pinned.

## Create the shared event

[Section titled “Create the shared event”](#create-the-shared-event)

src/examples/quickstart/python.py

```python
from veritio import create_audit_event, hash_audit_event

event = create_audit_event(
    {
        "id": "evt_member_invited_01",
        "occurredAt": "2026-08-09T10:00:00.000Z",
        "actor": {"type": "user", "id": "usr_123"},
        "action": "organization.member.invited",
        "target": {"type": "organization", "id": "org_acme"},
        "scope": {"tenantId": "org_acme", "environment": "production"},
        "purpose": "access_management",
        "lawfulBasis": "contract",
        "retention": "security_1y",
        "metadata": {"role": "viewer"},
    }
)

event_hash = hash_audit_event(event)
print({"eventId": event["id"], "hashPrefix": event_hash[:12]})
```

Terminal window

```sh
python3 src/examples/quickstart/python.py
```

verified output

```text
{'eventId': 'evt_member_invited_01', 'hashPrefix': '3c1eb99f7a8f'}
```

`create_audit_event` fails closed before it produces anything. It raises on a missing `actor.id`, `actor.type`, `action`, `target.id`, or `target.type`, and on an `action` that does not match the dotted lowercase protocol form. It sorts and de-duplicates `dataCategories`, drops `None` fields entirely, and passes `metadata` through deterministic key-pattern redaction before the value is ever hashed. Redaction happens on the way in, so a redacted event and its hash are consistent by construction; there is no separate “redact later” step to forget.

## What Python has and what it does not

[Section titled “What Python has and what it does not”](#what-python-has-and-what-it-does-not)

Capability

Python surface

Status

Events, edges, canonical JSON, hashing

`create_audit_event`, `create_evidence_edge`, `canonical_json`, `hash_audit_event`, `hash_audit_record`, `hash_evidence_edge`, `hash_evidence_edge_record`

present

Idempotency digest

`hash_idempotency_key`

present

Evidence commits

`create_evidence_commit`, `hash_evidence_commit`, `verify_evidence_commits`

present

Governed changes

`define_entity`, `create_governed_change_draft`, `create_governed_action_draft`, `governed_revision_id`

present

Risk

`normalize_risk_signals`, `score_risk_signals`, `rollup_episode_risk`, `risk_policy`, `band_of`, `with_risk_signals`

present

`security.risk` assertions

`create_security_risk_assertion`, `hash_assertion_record`, `build_security_risk_assessed_event`

present

Templates

26 builders plus `audit_templates`, `audit_template_sets`

present

Audit-record verifier

`verify_audit_records`

**absent — TypeScript only**

Edge-record verifier

`verify_evidence_edge_records`

**absent — TypeScript only**

Provenance recorder

`create_provenance_recorder`

**absent — TypeScript only**

Export bundles (`vevb-1`)

`build_export_bundle`, `verify_export_bundle`

**absent — TypeScript only**

Stores and recorders

`AuditStore`, `MemoryAuditStore`, `create_audit_recorder`

**absent — TypeScript only**

The four absent groups are not stubs that raise; the names do not exist. `from veritio import verify_audit_records` is an `ImportError`, not a runtime failure later.

### The verifier asymmetry

[Section titled “The verifier asymmetry”](#the-verifier-asymmetry)

Veritio has three independent chains. Python implements the verifier for exactly one of them.

```text
audit record chain   →  verify_audit_records          absent in Python
edge record chain    →  verify_evidence_edge_records  absent in Python
commit ledger        →  verify_evidence_commits       present in Python
```

`verify_evidence_commits` walks each `streamId` independently and returns `{"ok": True}` or `{"ok": False, "index": n, "reason": "..."}` at the first failure. It checks the hash algorithm, the `veritio-json-v1` canonicalization label, the `veritio-merkle-v1` tree algorithm, per-stream sequence continuity, previous-hash linkage, member-manifest normalization, `recordCount`, the recomputed `recordsRoot`, and the recomputed commit hash.

It deliberately does not reconcile member record hashes against independently verified records. A commit chain built entirely over fabricated record hashes verifies clean in isolation. The module’s own docstring says to compose it with `verify_audit_records` and `verify_evidence_edge_records` — functions that do not exist in this package. Treat a Python-side commit pass as a statement about the ledger, and run end-to-end record verification in a TypeScript process or against your own implementation of the rules in [Hash chain](/docs/concepts/hash-chain/).

## Risk scoring is protocol math, not a TypeScript detail

[Section titled “Risk scoring is protocol math, not a TypeScript detail”](#risk-scoring-is-protocol-math-not-a-typescript-detail)

`risk.py` is a byte-for-byte port of the TypeScript scorer. Every operation uses only clamp, floor, divide, and multiply — never `pow`, `exp`, or `log` — because those are the operations whose results are guaranteed to agree across language runtimes. Every constant lives in `DEFAULT_RISK_POLICY` under `policyVersion` `veritio.reference.v1`.

The parity fixture scores the same three pinned signal sets the TypeScript fixture scores and prints the score, level, policy version, and ordered factor contributions.

src/examples/risk/parity.py

```python
"""Cross-language parity twin of ``src/examples/risk/score-and-explain.ts``.

The invariant this fixture proves: risk scoring is language-neutral protocol
math, not a TypeScript implementation detail. It scores the SAME three pinned
signal sets as the TypeScript fixture, under the same reference policy
(``veritio.reference.v1``), and prints the score, level, policyVersion and the
ordered factor contributions. Every printed number must match the corresponding
value in ``src/examples/risk/expected/score-and-explain.txt`` exactly — if the
Python port of ``round4`` / ``_sat`` / ``clamp01`` ever drifts from the
TypeScript one, a documented score moves here and the two expected outputs stop
agreeing.

Two details make the comparison honest rather than cosmetic:

* The signal dicts are copied verbatim from the TypeScript fixture, including
  the deliberately sparse first case, so the fail-closed normalization defaults
  (recoverable / production / magnitude 0) are exercised in both languages.
* Numbers are rendered through ``canonical_json``, the cross-language byte
  contract, so a whole-valued float prints as ``1`` the way TS and Go emit it
  instead of Python's ``1.0``. Comparing raw ``repr()`` would report a false
  divergence on a score that is in fact byte-identical.

Deterministic by construction: literal inputs, pinned policy constants, no
clock, no randomness, no filesystem.

Run with the sibling SDK on the path:

    PYTHONPATH=../veritio/sdks/python/src python3 src/examples/risk/parity.py
"""

from typing import Any

from veritio import (
    DEFAULT_RISK_POLICY,
    canonical_json,
    normalize_risk_signals,
    score_risk_signals,
)

# Pinned verbatim from src/examples/risk/score-and-explain.ts. Do not "improve"
# these values: their only job is to be the same bytes the TypeScript fixture
# scores, so the two expected outputs can be diffed field by field.
RISK_SCENARIOS: list[dict[str, Any]] = [
    {
        "name": "read-config-lookup",
        "signals": {"operationType": "read"},
    },
    {
        "name": "bulk-export-staging",
        "signals": {
            "operationType": "bulk",
            "reversibility": "reversible",
            "envCriticality": "staging",
            "dataVolume": 5000,
            "fanOut": 3,
            "referenceCount": 12,
        },
    },
    {
        "name": "destructive-drop-production",
        "signals": {
            "operationType": "destructive",
            "reversibility": "irreversible",
            "envCriticality": "production",
            "dataVolume": 250000,
            "fanOut": 40,
            "referenceCount": 180,
        },
    },
]

def canonical_number(value: float) -> str:
    """Render one score/contribution the way canonical JSON hashes it.

    Parity is a claim about bytes, so the fixture must not print Python's float
    repr: ``clamp01`` returns ``1.0`` for a saturated score while TypeScript
    emits ``1``. ``canonical_json`` already coerces whole-valued finite floats to
    int for exactly this reason, so routing every number through it makes the
    printed line comparable to the TypeScript expected output instead of
    manufacturing a difference that no hash would ever see.
    """
    return canonical_json(value)

def factor_line(assessment: dict[str, Any]) -> str:
    """Flatten the ordered factor breakdown into one comparable line.

    Factor ORDER is part of the cross-language conformance contract, not a
    presentation choice, so the line is emitted in the order the scorer returns
    rather than sorted: a reordered breakdown must show up as a diff here.
    """
    return " ".join(
        f"{factor['key']}:{canonical_number(factor['contribution'])}"
        for factor in assessment["factors"]
    )

def main() -> None:
    """Print the parity report for every pinned scenario under the reference policy.

    Kept free of any ambient input (clock, environment, argv) so the program is
    byte-identical across runs and machines; the documentation pipeline diffs
    this stdout against expected/parity.txt.
    """
    print(f"policyVersion={DEFAULT_RISK_POLICY['policyVersion']}")
    print(f"bands={canonical_json(DEFAULT_RISK_POLICY['bands'])}")
    for scenario in RISK_SCENARIOS:
        assessment = score_risk_signals(scenario["signals"], DEFAULT_RISK_POLICY)
        print("")
        print(f"scenario={scenario['name']}")
        print(f"  signals={canonical_json(scenario['signals'])}")
        print(f"  normalized={canonical_json(normalize_risk_signals(scenario['signals']))}")
        print(f"  score={canonical_number(assessment['score'])}")
        print(f"  level={assessment['level']}")
        print(f"  policyVersion={assessment['policyVersion']}")
        print(f"  factors={factor_line(assessment)}")

if __name__ == "__main__":
    main()
```

verified output

```text
policyVersion=veritio.reference.v1
bands={"critical":0.75,"high":0.5,"low":0.05,"medium":0.25}

scenario=read-config-lookup
  signals={"operationType":"read"}
  normalized={"dataVolume":0,"envCriticality":"production","fanOut":0,"operationType":"read","referenceCount":0,"reversibility":"recoverable"}
  score=0.05
  level=low
  policyVersion=veritio.reference.v1
  factors=operationType:0.05 dataVolume:0 fanOut:0 referenceCount:0 reversibility:1 envCriticality:1

scenario=bulk-export-staging
  signals={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"}
  normalized={"dataVolume":5000,"envCriticality":"staging","fanOut":3,"operationType":"bulk","referenceCount":12,"reversibility":"reversible"}
  score=0.3718
  level=medium
  policyVersion=veritio.reference.v1
  factors=operationType:0.55 dataVolume:0.1961 fanOut:0.0129 referenceCount:0.0155 reversibility:0.6 envCriticality:0.8

scenario=destructive-drop-production
  signals={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"}
  normalized={"dataVolume":250000,"envCriticality":"production","fanOut":40,"operationType":"destructive","referenceCount":180,"reversibility":"irreversible"}
  score=1
  level=critical
  policyVersion=veritio.reference.v1
  factors=operationType:0.85 dataVolume:0.1999 fanOut:0.0738 referenceCount:0.0626 reversibility:1.3 envCriticality:1
```

Two details make this a real comparison rather than a cosmetic one. The first scenario is deliberately sparse, so the fail-closed normalization defaults — `reversibility: recoverable`, `envCriticality: production`, magnitudes `0` — are exercised in both languages instead of being supplied by the fixture. And every number is rendered through `canonical_json`, so a saturated score prints as `1` the way TypeScript emits it rather than as Python’s `1.0`. Comparing raw `repr()` would report a divergence on a score that is byte-identical under hashing.

Factor order is part of the contract. The line is emitted in the order the scorer returns, not sorted, so a reordered breakdown shows up as a diff.

`score_risk_signals(signals, policy=DEFAULT_RISK_POLICY)` takes the policy as an optional keyword, matching TypeScript. Go requires it explicitly. Python raises on an unknown `operationType`, `reversibility`, or `envCriticality`, and on a magnitude that is not a non-negative integer — it never silently degrades an unrecognized signal to a low score.

One small surface asymmetry: `clamp01` and `round4` are public in `veritio.risk` but are not re-exported from the package root, so `from veritio import round4` fails while `from veritio.risk import round4` works. `_sat` is private in both. `band_of` is re-exported from the root.

## Governed action drafts derive identical identifiers

[Section titled “Governed action drafts derive identical identifiers”](#governed-action-drafts-derive-identical-identifiers)

`create_governed_action_draft` is the host application’s server-side mutation boundary helper. It is present in Python with the same semantics as `createGovernedActionDraft` and `CreateGovernedActionDraft`, pinned by `spec/conformance/governed-action-draft.json`.

src/examples/governed/action-draft.py

```python
"""Cross-language parity twin of ``src/examples/governed/action-draft.ts``.

The invariant this fixture proves: governed-action derivation is
language-neutral protocol math, not a TypeScript implementation detail. It
drafts the SAME pinned subscription upgrade as the TypeScript fixture — the
same entity definition, the same before/after rows, the same
``occurredAt``/``idempotencyKey`` — and prints the derived identifiers a host
application depends on at its mutation boundary:

* ``create_governed_action_draft`` derives the change id and the activity id
  from ONE seed, sha256(``tenantId``:``idempotencyKey``), so a replayed
  mutation reproduces the same change/activity pair in Python that it does in
  TypeScript and Go instead of forking the graph per SDK.
* ``define_entity`` is the only place field capture is decided: the ``omit``
  field never reaches the state commitment (and so can never become a governed
  changed path), and the ``content_digest`` field is committed as a digest
  rather than a raw value.
* The state-commitment digest and the derived ``revisionId`` are byte-identical
  to the TypeScript ones, which is the real parity claim: canonical JSON,
  sha256 framing and field ordering agree across languages.
* The draft is INPUT ONLY. It carries no store-assigned sequence and no record
  hash; nothing is evidence until a conforming ``AuditStore`` appends it.

The printed JSON deliberately mirrors the TypeScript fixture's structure key
for key, so ``src/examples/governed/expected/action-draft.txt`` and
``src/examples/governed/expected/action-draft-python.txt`` can be diffed
directly. Exactly one line is allowed to differ: the prose ``note``, which
names the language's own entrypoint (``create_governed_action_draft`` vs
``createGovernedActionDraft``). Any diff on an id, digest, changed path, event
action or edge relation is a real cross-language divergence, not formatting.

Deterministic by construction: literal rows, a pinned ``occurredAt``, a pinned
idempotency key, no clock, no randomness, no filesystem. (``occurredAt`` must
stay pinned — ``create_governed_action_draft`` falls back to
``datetime.now(timezone.utc)`` when it is omitted.)

Run with the sibling SDK on the path:

    PYTHONPATH=../veritio/sdks/python/src python3 src/examples/governed/action-draft.py
"""

import json
from typing import Any

from veritio import create_governed_action_draft, define_entity

# Pinned verbatim from src/examples/governed/action-draft.ts. Do not "improve"
# these values: their only job is to be the same inputs the TypeScript fixture
# drafts, so the two expected outputs can be diffed field by field.
GOVERNED_SCOPE = {"tenantId": "org_acme", "environment": "production"}

SUBSCRIPTION = define_entity(
    authority="acme-billing",
    entity_type="subscription",
    schema_ref="acme://schemas/subscription@3",
    field_set_ref="acme://fieldsets/subscription-governed@1",
    identity=lambda row: row["id"],
    fields={
        "id": {"capture": "full"},
        "plan": {"capture": "full"},
        "seatCount": {"capture": "full"},
        "status": {"capture": "full"},
        # PII stays out of evidence as a value; only its digest is committed.
        "accountEmail": {"capture": "content_digest"},
        # Ungoverned operator prose never enters the commitment or changed paths.
        "internalNotes": {"capture": "omit"},
    },
)

BEFORE: dict[str, Any] = {
    "id": "sub_9f31",
    "accountEmail": "billing@acme.example",
    "plan": "team",
    "seatCount": 12,
    "status": "active",
    "internalNotes": "renewal call scheduled",
}

AFTER: dict[str, Any] = {
    **BEFORE,
    "plan": "enterprise",
    "seatCount": 25,
    "internalNotes": "upgrade approved on the renewal call",
}

def build_subscription_upgrade_draft() -> dict[str, Any]:
    """Build the pinned upgrade draft shared by the docs page and this fixture.

    Kept as a named function so the documentation can show one call site while
    CI re-runs the identical derivation. Every argument matches
    ``buildSubscriptionUpgradeDraft`` in the TypeScript twin, including
    ``occurredAt`` and ``idempotencyKey``, because both feed hashed evidence:
    ``occurredAt`` is stamped on every drafted event and edge, and
    ``idempotencyKey`` seeds the change/activity ids and the tenant-scoped
    idempotency hash.
    """
    return create_governed_action_draft(
        {
            "scope": GOVERNED_SCOPE,
            "entity": SUBSCRIPTION,
            "before": BEFORE,
            "after": AFTER,
            "actionType": "subscription.upgraded",
            "activityType": "billing.plan_change",
            "initiatedBy": {
                "authority": "acme-billing",
                "kind": "principal",
                "type": "user",
                "id": "usr_owner",
            },
            "performedBy": {
                "authority": "acme-billing",
                "kind": "principal",
                "type": "service",
                "id": "svc_billing_api",
            },
            "producer": {
                "authority": "acme-billing",
                "kind": "principal",
                "type": "service",
                "id": "svc_billing_api",
            },
            "occurredAt": "2026-08-09T10:00:00.000Z",
            "idempotencyKey": "subscription.upgraded:sub_9f31:req_7c2a",
            "mutationBinding": "same_transaction",
        }
    )

def main() -> None:
    """Print the governed-action parity report for the pinned upgrade.

    Kept free of any ambient input (clock, environment, argv) so the program is
    byte-identical across runs and machines; the documentation pipeline diffs
    this stdout against expected/action-draft-python.txt, and a reviewer diffs
    that file against the TypeScript twin's expected output.
    """
    draft = build_subscription_upgrade_draft()
    change_id = draft["changeRef"]["id"]
    id_seed = change_id[change_id.rfind("_") + 1 :]
    entity_id = draft["entityRef"]["id"]
    commitment = draft["revision"]["stateCommitment"]

    output = {
        "derivedIds": {
            "changeId": change_id,
            "activityId": draft["activityRef"]["id"],
            "entityId": entity_id,
            "revisionId": draft["revision"]["ref"]["id"],
            "idSeed": id_seed,
            # Both ids are `<prefix>_<entityType>_<entityId>_<seed>` off the same
            # sha256(tenantId:idempotencyKey) seed, so replay is idempotent.
            "changeAndActivityShareOneSeed": (
                change_id == f"chg_subscription_{entity_id}_{id_seed}"
                and draft["activityRef"]["id"] == f"act_subscription_{entity_id}_{id_seed}"
            ),
        },
        "changedPaths": draft["revision"]["changedPaths"],
        "stateCommitment": {
            "algorithm": commitment["algorithm"],
            "canonicalization": commitment["canonicalization"],
            "schemaRef": commitment["schemaRef"],
            "fieldSetRef": commitment["fieldSetRef"],
            "digest": commitment["digest"],
            "committedFields": sorted(commitment["fields"].keys()),
            # `internalNotes` is declared `omit`, so it is absent from the
            # commitment and can never become a governed changed path.
            "omittedFieldPresent": "internalNotes" in commitment["fields"],
            "accountEmailCommitment": commitment["fields"]["accountEmail"],
        },
        "eventActions": [event["action"] for event in draft["events"]],
        "edgeRelations": [edge["relation"] for edge in draft["edges"]],
        "draftIsInputOnly": {
            "note": "create_governed_action_draft returns evidence INPUTS, not persisted records. Append draft['outboxEntry'] through a conforming AuditStore inside the same mutation to make it evidence.",
            "eventsCarryStoreAssignedSequence": any(
                "sequence" in event for event in draft["events"]
            ),
            "eventsCarryRecordHash": any("hash" in event for event in draft["events"]),
            "outboxMutationBinding": draft["outboxEntry"]["mutationBinding"],
            "outboxSchemaVersion": draft["outboxEntry"]["schemaVersion"],
        },
    }

    print(json.dumps(output, indent=2))

if __name__ == "__main__":
    main()
```

verified output

```json
{
  "derivedIds": {
    "changeId": "chg_subscription_sub_9f31_312ca4bde590b545",
    "activityId": "act_subscription_sub_9f31_312ca4bde590b545",
    "entityId": "sub_9f31",
    "revisionId": "rev_subscription_sub_9f31_a71e2768812c_fe34f6d0",
    "idSeed": "312ca4bde590b545",
    "changeAndActivityShareOneSeed": true
  },
  "changedPaths": [
    "/plan",
    "/seatCount"
  ],
  "stateCommitment": {
    "algorithm": "sha256",
    "canonicalization": "veritio-json-v1",
    "schemaRef": "acme://schemas/subscription@3",
    "fieldSetRef": "acme://fieldsets/subscription-governed@1",
    "digest": "sha256:a71e2768812cca4e3609ce7e24460967d253e1ddab13d9a8898f4fd31a3564cb",
    "committedFields": [
      "accountEmail",
      "id",
      "plan",
      "seatCount",
      "status"
    ],
    "omittedFieldPresent": false,
    "accountEmailCommitment": {
      "captureMode": "content_digest",
      "digest": "sha256:aa69b0bc0b21a0ca7b5b5d1beb7556eb7d598f21d638db3eb9770ddd4fb30e45"
    }
  },
  "eventActions": [
    "change.declared",
    "activity.recorded",
    "entity.revision.created"
  ],
  "edgeRelations": [
    "has_activity",
    "has_output",
    "performed_by",
    "generated"
  ],
  "draftIsInputOnly": {
    "note": "create_governed_action_draft returns evidence INPUTS, not persisted records. Append draft['outboxEntry'] through a conforming AuditStore inside the same mutation to make it evidence.",
    "eventsCarryStoreAssignedSequence": false,
    "eventsCarryRecordHash": false,
    "outboxMutationBinding": "same_transaction",
    "outboxSchemaVersion": "2026-06-23"
  }
}
```

Three invariants are visible in that output.

**One seed drives both identifiers.** The change id and the activity id are both `<prefix>_<entityType>_<entityId>_<seed>` over the same `sha256(tenantId:idempotencyKey)` digest, so a replayed mutation reproduces the same change and activity pair in Python that it does in TypeScript instead of forking the evidence graph per SDK.

**`define_entity` is the only place field capture is decided.** `internalNotes` is declared `omit`, so it never enters the state commitment and can never become a governed changed path — `omittedFieldPresent` is `false` and `changedPaths` contains only `/plan` and `/seatCount` even though the operator prose also changed. `accountEmail` is declared `content_digest`, so the commitment carries a digest envelope rather than the address.

**A draft is input, not evidence.** `eventsCarryStoreAssignedSequence` and `eventsCarryRecordHash` are both `false`. The draft has no sequence and no record hash because nothing has been appended yet. Python cannot close that gap in-process: there is no conforming `AuditStore` in this package, so `draft["outboxEntry"]` has to be written by your own transaction and drained to a store a TypeScript process owns, or to the hosted ingest path. The `mutationBinding` of `same_transaction` is a declaration about how you persist it, not something the SDK enforces.

`create_governed_action_draft` raises when no governed field changes, when a required ref is missing, and when a `keyed_digest` field is declared without `digestKeys.keyedDigest`. Pin `occurredAt` in tests: when omitted it falls back to `datetime.now(timezone.utc)` and the draft stops being reproducible.

## Templates: same count, one different name

[Section titled “Templates: same count, one different name”](#templates-same-count-one-different-name)

Python exports 26 event builders, the same 26 TypeScript exports, grouped by `audit_template_sets` into `auth`, `organization`, `data`, `agent`, and `code`. They are a convenience layer over `create_audit_event`, and they are **not** pinned by conformance fixtures. Nothing in CI would notice if one language’s template drifted in the metadata it stamps.

One name genuinely diverges:

```text
TypeScript  episodeStartedTemplate
Go          EpisodeStartedTemplate
Python      activity_episode_started_template
```

The emitted event is the same. A mechanical camelCase-to-snake\_case translation will miss it, which is exactly the failure mode to expect when porting a TypeScript agent-instrumentation module. The builder stamps `activityEpisodeId` as a reserved, un-shadowable grouping key, along with `authSessionId` and `authContextId` in reserved metadata that caller-supplied metadata cannot override.

## Failure behavior

[Section titled “Failure behavior”](#failure-behavior)

The SDK raises rather than degrading. `create_audit_event` rejects missing identities and malformed actions. `create_evidence_edge` rejects relations outside the supported vocabulary and entity types outside the evidence entity set. `create_evidence_commit` rejects a non-positive sequence and a `previousCommitHash` that is neither `None` nor a `sha256:` digest. `canonical_json` rejects non-finite numbers instead of inventing an unstable representation for them.

Never replace one of those exceptions with an empty tenant, a default actor, or a partially normalized dictionary. An event that was recorded with a substituted identity is worse than an event that was never recorded, because the chain will verify and the claim will be wrong.

## Verify your checkout

[Section titled “Verify your checkout”](#verify-your-checkout)

Run the upstream suite against the pinned source:

Terminal window

```sh
PYTHONPATH=sdks/python/src python3 -m unittest discover -s sdks/python/tests
```

Then run `bun run verify:examples` in this website to re-execute both fixtures on this page and diff their stdout against the committed expected output.

## Where to go next

[Section titled “Where to go next”](#where-to-go-next)

Read [Cross-language parity matrix](/docs/reference/parity-matrix/) for the full capability table across all three SDKs, including which behaviors are pinned by shared fixtures and which are merely present. Use [FastAPI](/docs/frameworks/fastapi/) for a complete Python application example. If your architecture needs the conforming authoritative-store interface or a record-chain verifier in-process, take the TypeScript path or implement the missing capability in the OSS repository before documenting it as available.

[Edit page](https://github.com/getveritio/veritio-website/edit/main/src/content/docs/docs/sdks/python.mdx)

Last updated: Aug 23, 2026

[Previous  
TypeScript](/docs/sdks/typescript/)[Next  
Go](/docs/sdks/go/)

Veritio provides evidence support, not legal advice or automatic compliance.

This site uses cookieless, anonymous analytics (Umami) by default. With your consent, we also enable Google Analytics, which sets cookies and sends usage data to Google. [Privacy Policy](/legal/privacy/)
