Synchronous client for Python 3.10+, built on httpx and cryptography only. Request signing, local hashing, contract-respecting retries, selective disclosure and webhook verification are built in. The client is a context manager (with RoxRegister(...) as client:). You need a vault API key and a registered request-signing key, both created in your Platform.
Install
The signing key is passed as (kid, private_jwk_or_pem); Ed25519 and P-256 keys are both supported, the SDK picks the right signing mode automatically.
The complete happy path: construct the client, seal a document digest, observe the terminal outcome, verify the bytes. A seal request is validated and either rejected immediately or accepted with 202; every accepted record then reaches exactly one terminal outcome, anchored with proof or failed, delivered by webhook or polling. There are no anchoring options and no timing promises.
Python
import json, os
from rox_register import RoxRegister
private_jwk = json.load(open("request-key.jwk")) # private JWK dict (or a PEM string)
client = RoxRegister(
api_key=os.environ["RR_API_KEY"], # rr_live_ak_... (vault API key)
request_signing_key=("ish-prod-2026-01", private_jwk), # (kid, key material)
)
# 1 · Seal. The SDK hashes the bytes locally; the file never leaves
# your process. Validated and either rejected immediately or
# accepted with 202.
pdf = open("policy.pdf", "rb").read()
accepted = client.seal(
"policy",
content=pdf, # or content_digest_hex="..."
external_ref="POL-2026-88123",
idempotency_key="POL-2026-88123-v1", # safe retries for 48h
)
print(accepted["reference"]) # rr_ref_01J8ZKQ2...
salt = accepted["salt_hex"] # STORE IT
# 2 · Exactly one terminal outcome follows asynchronously, anchored or
# failed, via the record.anchored / record.failed webhooks, or poll:
record = client.get_record(accepted["record_id"])
# record["status"]: pending and batched are pre-anchor states, treat them
# identically; anchored and failed are the terminal outcomes.
# 3 · Anyone can verify the exact bytes, no API key needed:
check = client.verify_file(pdf, salt, reference=accepted["reference"])
assert check["match"] and check["result"] == "genuine"
Consume webhooks
verify_webhook_signature compares the HMAC constant-time and enforces the freshness window (default 300 seconds). Always feed it the raw body bytes; re-serialized JSON breaks the MAC. Full delivery contract on the webhooks page; end-to-end registration in the webhooks guide.
Python
from flask import Flask, request, abort
from rox_register import verify_webhook_signature
app = Flask(__name__)
@app.post("/rox/events")
def rox_events():
raw = request.get_data() # EXACT raw body bytes, before any parsing
if not verify_webhook_signature(
secret=WEBHOOK_SECRET, # whsec_..., shown once at creation
headers=request.headers,
raw_body=raw, # constant-time compare + 300s freshness
):
abort(401)
event = request.get_json() # {id, type, created_at, data}
if event["type"] == "record.anchored":
mark_proven(event["data"])
if event["type"] == "record.failed":
escalate(event["data"])
if event["type"] == "verification.mismatch":
alert_security(event["data"])
return "", 200 # non-2xx deliveries are retried, then marked dead
Selective disclosure
Commit salted hashes of individual fields at sealing time; values and field salts never reach Rox. The canonical value encoding matches the Node SDK byte for byte, so hashes are portable across stacks. The full walkthrough is the selective disclosure guide.
Python
from rox_register import hash_fields, compute_field_root
# Sealing side: hash the fields locally. Values and per-field salts
# NEVER reach Rox, only the salted hashes do.
field_hashes, field_salts, field_root = hash_fields([
("policy_no", "POL-2026-88123"),
("insured_name", "Aya Hassan"),
("coverage_egp", 250000),
])
client.seal("policy", content=pdf, field_hashes=field_hashes)
# Retain field_salts (and the values) alongside your document.
# Verifier side: recompute the root from the salted hashes and compare
# with field_root_hex from the issuer's record view or the RRP-1 proof
# bundle, fully offline.
assert compute_field_root([f["salted_hash_hex"] for f in field_hashes]) \
== record["field_root_hex"]
Error handling
Every non-2xx response raises a typed subclass of RoxRegisterError carrying the RFC 7807 problem document and its stable RR-code. 429 responses are retried automatically honoring Retry-After; 5xx and transport errors are retried only when the call is safe to repeat.
Python
from rox_register import AuthError, StateError, ValidationError, RoxRegisterError
try:
client.seal("policy", content_digest_hex=digest)
except AuthError as e: # RR-1001..1101: key, scope, JWS problems
rotate_credentials_alert(e.code)
except ValidationError as e: # RR-2xxx: fix your request
fix_request(e.code, e.detail)
except StateError as e: # vault paused, approval required/expired
request_approval_or_unpause(e.code)
except RoxRegisterError as e: # anything else; e.retry_after on 429/5xx
schedule_retry(e.retry_after)
# Every error carries the RFC 7807 problem document:
# e.code, e.status, e.title, e.detail
Client surface
Method names mirror the API reference; reads need only the API key, mutations additionally require the request-signing key configured on the client. Reads are sent unsigned by design (the API key authorizes them); a signature on a read is optional but validated when presented, so a signed read doubles as a live check that your kid is still active.
Sealing
seal(record_type, *, content=..., ...)POST /v1/seal. Pass content (hashed locally) or content_digest_hex. Returns the 202 acceptance dict including salt_hex.
seal_batch(items)POST /v1/seal/batch, up to 1,000 items under one JWS; per-item idempotency_key; results in request order.
Records
get_record(id_or_reference)GET /v1/records/{id}. Status, digest, salt, leaf hash and, once anchored, the anchored object (chain tx, slot, block time, cluster), the same shape the public verify plane serves.
list_records(**filters)GET /v1/records, newest first, cursor pagination with status / type / ref / date filters.
Sign envelopes (sign scope)
create_envelope(name, record_type, *, file=..., signers=..., signing_order=...)POST /v1/envelopes. Signed multipart create-and-send: source PDF plus signers and field placements; the envelope lands in status sent. Requires the sign_ses entitlement (RR-3101) and holds the service price in ROX (RR-3102 names the shortfall); the hold is captured only on completion.
get_envelope(envelope_id)GET /v1/envelopes/{id}. Signers with signing_url, field layout, last 100 events, and, once assembled, the expiring final_file_url / certificate_file_url downloads plus the sealed record ids.
list_envelopes(*, cursor=None, limit=None, status=None)GET /v1/envelopes, newest first, cursor pagination with an optional status filter.
void_envelope(envelope_id, reason)POST /v1/envelopes/{id}/void. Withdraws a sent envelope; the hold is released and nothing is charged.
assemble_envelope(envelope_id)POST /v1/envelopes/{id}/assemble. Idempotent assembly recovery after an assembly_failed event; never captures the fee twice.
Verify plane (public, no key)
verify_hash(content_digest_hex, salt_hex, *, reference=None)POST /verify/hash. Byte-exact match check.
verify_file(file, salt_hex, *, reference=None)POST /verify/file. Bytes or a path; the server stream-hashes in memory and never stores the upload.
Vault and keys
get_vault()GET /v1/vault, the calling key's vault profile: identity, status, environment, current authority key.
create_vault(name, slug, *, first_request_key, description=None)POST /v1/vaults (privileged, vaults.manage scope). Creates a vault, queues its DKG and returns a one-time API key for it. The vaults.manage scope is deliberately narrow: it grants vault creation ONLY, so a vaults.manage key is refused reads like getVault with RR-1005; that refusal is scoping, not a key problem. Use a read-scoped key for reads.
get_gas()GET /v1/gas: the ROX deposit wallet (address, balance in lamports and as a ROX string) and the RoxChain cluster. Requires the billing.read scope, granted explicitly on the key; seal or read keys do not gain it implicitly (RR-1005 otherwise).
get_gas_ledger(*, cursor=None, limit=None)GET /v1/gas/ledger: cursor-paginated cost ledger, newest first, with exact lamports and ROX string amounts plus the anchor tx signature each entry paid for.
Webhooks
create_webhook(url, events, *, vault_id=None)POST /v1/webhooks. The returned secret is shown exactly once.
list_webhooks() / webhook_deliveries(webhook_id)Endpoint list and per-endpoint delivery log, cursor-paginated.