client.attest()
The headline call. Runs a payload through the orchestrator's nine stages and returns a sealed receipt containing the manifest hash.
Signature
client.attest(
payload: str | bytes,
program_id: str,
route_id: str,
registry_record_hash: str,
attested_context: AttestedContext | None = None,
reproducibility_context: Any | None = None,
ai_output_text: str | None = None,
) -> AttestationReceipt
| Argument | Type | HTTP wire format | Notes |
|---|---|---|---|
payload | str or bytes | payload_text (string) or payload (string) | Strings sent as payload_text; UTF-8-decodable bytes are decoded and sent as payload_text; other bytes are base64-encoded and sent as payload. The wire format is always a string. |
program_id | str | program_id | 64-char lowercase hex |
route_id | str | route_id | Non-empty |
registry_record_hash | str | registry_record_hash | 64-char lowercase hex |
Payload encoding
The HTTP request body sends payload as a JSON string, not as bytes. The SDK's handling:
- String input: Sent as
payload_textin the JSON request. - Bytes input (UTF-8 decodable): Decoded to string, then sent as
payload_text(same as string case). - Bytes input (non-UTF-8): Encoded as base64, then sent as the
payloadfield (string, not bytes).
The orchestrator's DOG stage hashes the string it receives. If that string parses as a single JSON value, it is canonicalized under RFC 8785 (JCS) and hashed in the dog.v1.jcs mode; otherwise its raw UTF-8 bytes are hashed in the dog.v1.raw mode. A base64 payload is not decoded: the base64 string itself is what gets hashed.
This means the bytes that get hashed depend on your input type. To ensure determinism across language implementations, always pass strings if your content is text.
Return value
AttestationReceipt. See AttestationReceipt for full field reference. Most callers just want receipt.manifest_hash.
Raises
| Exception | When |
|---|---|
ValidationError | Client-side validation failed (bad hex, empty route, wrong type) |
StageError | A specific pipeline stage rejected the run; check .stage |
APIError | API returned a non-2xx response that wasn't a stage error |
ConnectionError | Couldn't reach the API |
All four inherit from VerdifaxError for single-except handling.
Example
from verdifax import VerdifaxClient
with VerdifaxClient() as client:
receipt = client.attest(
payload="hello verdifax",
program_id="a" * 64,
route_id="route-test",
registry_record_hash="b" * 64,
)
print(receipt.manifest_hash)
Canonical request body
Identical inputs produce an identical canonical request body. The SDK enforces this by canonicalizing the request body, payload_text always wins over payload when the input is a UTF-8 string.
Each call still seals its own run. The orchestrator mints a fresh admission clock and per-run nonce for every /execute call, so two calls with identical inputs return different manifest_hash values; see What the Manifest Hash proves.
Module-level shortcut
For one-shot scripts, verdifax.attest(...) is a convenience that constructs a client from env vars, runs the call, and closes the client:
import verdifax
receipt = verdifax.attest(
payload="hi", program_id="a"*64, route_id="r", registry_record_hash="b"*64,
)
For high-throughput use, instantiate VerdifaxClient yourself and reuse.
