Python SDK reference
This page documents the public Python surface shipped in kit/python and the shared signing rules from crates/paybond-kit. For narrative walkthroughs, start with the Python quickstart.
Every Kit coroutine that talks to Harbor is backed by an OpenAPI operation. The canonical contract lives in docs/api/harbor-openapi.yaml; operation ids used below are linked via the rendered Harbor API reference with anchors like #operation/harborCreateIntent.
Scope note: this SDK currently wraps Harbor execution flows, tenant-scoped ledger provenance reads, and read-only tenant-bound Signal reads.
All I/O is async. The Kit does not expose a sync façade today.
Module entry point
from paybond_kit import (
Paybond,
PaybondIntents,
PaybondCapabilityBinding,
ServiceAccountHarborSession,
ServiceAccountSignalSession,
GatewayHarborTokenProvider,
HarborClient,
GatewaySignalClient,
VerifyCapabilityResult,
HarborHttpError,
GatewayAuthError,
SignalHttpError,
TenantBindingError,
sign_payee_evidence_binding,
)High-level client
Paybond.open
@classmethod
async def Paybond.open(
cls,
*,
gateway_base_url: str,
api_key: str, # "paybond_sk_…" service-account key
harbor_base_url: str,
harbor_access_path: str = "/v1/auth/harbor-access",
clock_skew_seconds: float = 90.0,
max_retries: int = 3,
) -> Paybond: ...Exchanges the service-account key at the Gateway (POST /v1/auth/harbor-access), caches the echoed tenant_id, and returns a Paybond instance bound to that tenant realm. Underneath it constructs a GatewayHarborTokenProvider and a HarborClient.
Raises GatewayAuthError on any token-exchange failure.
Tenant isolation: the returned session is bound to the Gateway-derived tenant_id. Do not reuse the instance across tenants — build one per (tenant, service account).
Paybond instance
@dataclass
class Paybond:
harbor: HarborClient # tenant-bound low-level client
intents: PaybondIntents # ergonomic intent helpers
async def rotate_harbor_token(self) -> None: ...
async def aclose(self) -> None: ...paybond.harbor— the tenant-boundHarborClient. Use for operations the high-levelintentshelper does not wrap (for exampleverify_capability).paybond.intents— seePaybondIntents.paybond.rotate_harbor_token()— force the Gateway exchange to rotate the cached Harbor JWT.paybond.aclose()— release HTTP resources (httpx.AsyncClient.aclose). Always await in afinallyblock orasync with contextlib.aclosing(...).
PaybondIntents
paybond.intents wraps the two principal/payee signing flows. Both methods derive tenant scope from the bound Harbor client; callers never pass a tenant id.
paybond.intents.create
async def create(
self,
*,
principal_did: str,
principal_signing_seed: bytes, # exactly 32 bytes
payee_did: str,
budget: Mapping[str, Any], # canonical JSON (e.g. {"currency": ..., "max_spend_usd": ...})
predicate: Mapping[str, Any], # predicate_dsl (version 1 and root node)
currency: str,
amount_cents: int,
evidence_schema: Mapping[str, Any], # JSON schema for evidence payloads
deadline_rfc3339: str,
allowed_tools: list[str],
intent_id: UUID | None = None, # defaults to uuid4()
predicate_ref: str = "", # managed-policy digest (v3) when set
idempotency_key: str | None = None,
) -> dict[str, Any]: ...Builds a principal-signed POST /intents body via the bundled _native.build_signed_create_intent_json (shared rules in crates/paybond-kit) and POSTs it through HarborClient.create_intent. Maps to Harbor operation harborCreateIntent.
Requires the bundled native extension. Published wheels include it; source checkouts should run maturin develop. Without it an ImportError is raised on the first call.
Retries honor idempotency_key on transient 5xx/429. Reusing a key with a different body returns HTTP 409 from Harbor (see docs/api/harbor-idempotency-openapi.yaml).
allowed_tools is a caller-defined allow-list of delegated operation names such as travel.planner.plan or crm.contact.enrich. Paybond does not define these strings; Harbor only checks that later verify calls use a matching value.
paybond.intents.submit_evidence
async def submit_evidence(
self,
intent_id: UUID,
payload: Mapping[str, Any], # evidence JSON matching the intent's schema
*,
payee_did: str,
payee_signing_seed: bytes, # exactly 32 bytes
artifacts_blake3_hex: list[str] | None = None,
submitted_at_rfc3339: str | None = None, # defaults to now (UTC)
idempotency_key: str | None = None,
) -> dict[str, Any]: ...Signs the payee binding via sign_payee_evidence_binding and POSTs it through HarborClient.submit_evidence. Maps to Harbor operation harborSubmitEvidence.
The response includes state, tenant, and an optional predicate_passed; a False here is not an error — the predicate evaluation failed and the intent moved to evidence_submitted with the recorded failure.
Verify (pass-through)
The Kit exposes capability verification on the underlying Harbor client rather than on paybond.intents:
await paybond.harbor.verify_capability(
intent_id=intent_id,
token=capability_token,
operation="payments.capture",
requested_spend_cents=18_700,
)Maps to Harbor operation harborVerifyCapability. Returns a VerifyCapabilityResult with allow=False when Harbor denies — that is not raised.
Confirm settlement (not wrapped)
Settlement confirmation is an operator-tier action today and is not wrapped by the Kit. Call Harbor directly for operation harborConfirmSettlement:
import httpx
async with httpx.AsyncClient(timeout=30.0) as http:
response = await http.post(
f"{harbor_base}/intents/{intent_id}/settlement/confirm",
headers={
"authorization": f"Bearer {await harbor_bearer()}",
"content-type": "application/json",
"x-tenant-id": paybond.harbor.tenant_id,
"idempotency-key": settlement_idempotency_key,
},
json={"outcome": "release"}, # or {"outcome": "refund"}
)
response.raise_for_status()Prefer the Operator Console for human-audited settlement.
Low-level session
ServiceAccountHarborSession
class ServiceAccountHarborSession:
@classmethod
async def open(
cls,
*,
gateway_base_url: str,
api_key: str,
harbor_base_url: str,
harbor_access_path: str = "/v1/auth/harbor-access",
clock_skew_seconds: float = 90.0,
max_retries: int = 3,
) -> ServiceAccountHarborSession: ...
harbor: HarborClient
async def rotate_harbor_token(self) -> None: ...
async def aclose(self) -> None: ...Same lifecycle as Paybond.open without the ergonomic intents helper. Use it when you only need raw HarborClient methods (for example verify flows inside agent guardrails).
GatewayHarborTokenProvider
class GatewayHarborTokenProvider:
def __init__(
self,
*,
gateway_base_url: str,
api_key: str,
harbor_access_path: str = "/v1/auth/harbor-access",
clock_skew_seconds: float = 90.0,
) -> None: ...
@property
def tenant_id(self) -> str | None: ...
async def ensure_initial(self) -> str: ...
async def bearer(self) -> str: ...
async def force_rotate(self) -> None: ...Owns the cached Gateway exchange. Missing tenant_id or access_token in the Gateway response raises GatewayAuthError (requires Gateway V1-008+).
HarborClient
Tenant-bound async HTTP client. Sends x-tenant-id on every request; verifies Harbor echoes the same tenant and the requested intent_id on POST /verify (confused-deputy hardening — raises TenantBindingError on mismatch).
class HarborClient:
def __init__(
self,
harbor_base: str,
tenant_id: str,
*,
harbor_bearer_supplier: Callable[[], Awaitable[str | None]] | None = None,
static_harbor_bearer_token: str | None = None,
max_retries: int = 3,
request_timeout_sec: float = 30.0,
) -> None: ...
@property
def tenant_id(self) -> str: ...
async def verify_capability(
self,
*,
intent_id: UUID,
token: str,
operation: str,
requested_spend_cents: int = 0,
) -> VerifyCapabilityResult: ... # harborVerifyCapability
async def create_intent(
self,
body: dict[str, Any],
*,
idempotency_key: str | None = None,
) -> dict[str, Any]: ... # harborCreateIntent
async def submit_evidence(
self,
intent_id: UUID,
evidence_body: dict[str, Any],
*,
idempotency_key: str | None = None,
) -> dict[str, Any]: ... # harborSubmitEvidence
async def get_ledger_tip(self) -> dict[str, Any]: ...
async def get_ledger_authority(self) -> dict[str, Any]: ...
async def get_ledger_events(
self,
*,
after_seq: int = 0,
limit: int = 64,
) -> dict[str, Any]: ...
async def get_ledger_merkle_latest(self) -> dict[str, Any]: ...
async def aclose(self) -> None: ...Retries 429 / 500 / 502 / 503 / 504 with exponential backoff and jitter, honoring a bounded Retry-After. All other 4xx/5xx raise HarborHttpError.
Ledger reads
The SDK includes tenant-scoped provenance reads on HarborClient:
get_ledger_tip()→GET /ledger/v1/tipget_ledger_authority()→GET /ledger/v1/authorityget_ledger_events(after_seq=..., limit=...)→GET /ledger/v1/eventsget_ledger_merkle_latest()→GET /ledger/v1/merkle/latest
Each method validates the echoed tenant_id against the bound client tenant and fails closed on mismatch.
Signal reads
The Python SDK ships a tenant-bound gateway Signal client for the supported read-only V1.0.1 surfaces.
GatewaySignalClient
class GatewaySignalClient:
def __init__(
self,
gateway_base_url: str,
tenant_id: str,
*,
static_gateway_bearer_token: str,
max_retries: int = 3,
request_timeout_sec: float = 30.0,
) -> None: ...
@property
def tenant_id(self) -> str: ...
async def get_reputation_receipt(
self,
operator_did: str,
*,
score_version: str | None = None,
) -> dict[str, Any] | None: ...
async def get_portfolio_summary(
self,
*,
score_version: str | None = None,
) -> dict[str, Any]: ...
async def get_operator_explanation(
self,
operator_did: str,
*,
score_version: str | None = None,
) -> dict[str, Any] | None: ...
async def get_operator_review_status(
self,
operator_did: str,
*,
score_version: str | None = None,
) -> dict[str, Any] | None: ...
async def aclose(self) -> None: ...This client validates the echoed tenant_id and operator_did on every response. 404 remains a None result for receipt, explanation, and review-status lookups; other non-success statuses raise SignalHttpError.
ServiceAccountSignalSession
@dataclass
class ServiceAccountSignalSession:
signal: GatewaySignalClient
@classmethod
async def open(
cls,
*,
gateway_base_url: str,
api_key: str,
principal_path: str = "/v1/auth/principal",
max_retries: int = 3,
) -> ServiceAccountSignalSession: ...
async def aclose(self) -> None: ...This is the easiest tenant-safe path for partner developers who only need Signal reads. The session resolves the tenant from GET /v1/auth/principal using the same service-account credential instead of relying on ad hoc tenant headers.
PaybondCapabilityBinding
@dataclass
class PaybondCapabilityBinding:
harbor: HarborClient
intent_id: UUID
capability_token: strBinds (harbor, intent_id, capability_token) so agent tool wrappers have a single tenant-safe handle to verify against. Wired into OpenAI Agents SDK via paybond_capability_input_guardrail and into LangGraph via paybond_awrap_tool_call_capability — see Agent integrations.
Result types
VerifyCapabilityResult
@dataclass(frozen=True)
class VerifyCapabilityResult:
allow: bool
audit_id: UUID
tenant: str
intent_id: UUID
code: str | None
message: str | NoneEvidence response
paybond.intents.submit_evidence returns a plain dict[str, Any] with at least the following keys:
| Key | Type | Meaning |
|---|---|---|
intent_id | str (UUID) | Harbor-echoed intent id; compared against the request. |
tenant | str | Harbor-echoed tenant realm. |
state | str | New intent state (for example "evidence_submitted"). |
predicate_passed | bool (optional) | True if the predicate VM accepted the evidence. |
Canonical signing helpers
sign_payee_evidence_binding
def sign_payee_evidence_binding(
*,
tenant_id: str,
intent_id: UUID,
payee_did: str,
payload: dict[str, Any],
artifacts_blake3_hex: list[str],
submitted_at_rfc3339: str,
payee_signing_seed: bytes,
) -> dict[str, Any]: ...Builds the signed evidence wire body used by operation harborSubmitEvidence. Exported for advanced callers that pre-build evidence bodies outside of paybond.intents.submit_evidence.
Principal-intent signing is performed in the native extension via _native.build_signed_create_intent_json and is documented inline in kit/python/src/paybond_kit/paybond.py. Canonical JSON normalisation, BLAKE3 digests, and Ed25519 signing match crates/paybond-kit byte-for-byte.
Typed errors
All error classes extend RuntimeError and attach structured diagnostics. Never log raw tokens or API keys; the Kit performs no redaction itself.
HarborHttpError
class HarborHttpError(RuntimeError):
status_code: int
url: str
body_text: str # raw Harbor response body (may be JSON)Raised by HarborClient when Harbor returns any non-success status after retries. Look for the machine-readable code field inside body_text — contracts are in the Error envelope reference.
GatewayAuthError
class GatewayAuthError(RuntimeError):
status_code: int | None
body_text: str | NoneRaised when /v1/auth/harbor-access rejects the API key, returns malformed JSON, or omits access_token / tenant_id. A missing tenant_id is logged as PAYBOND-V1-008 — upgrade the Gateway if you see it.
SignalHttpError
class SignalHttpError(RuntimeError):
status_code: int
url: str
body_text: strRaised by GatewaySignalClient when gateway Signal routes return a non-success status after retries, excluding the 404 -> None read contract on receipt, explanation, and review-status lookups.
TenantBindingError
class TenantBindingError(RuntimeError): ...Raised when Harbor echoes a different tenant or intent than the Kit requested on POST /verify. Treat as severity-zero tenant-isolation incidents and do not retry: review docs/security/tenant-scoping.md before shipping the fix.
Retries, idempotency, and logging
- All mutating coroutines accept
idempotency_key. The same key with the same body is safe to retry on transient failures; the same key with a different body returns409. - Log
tenant_id,intent_id,status_code,url, correlation id (when present), and a redactedbody_text. Never log the API key, Harbor JWT, or capability token. - Backoff is exponential with jitter, bounded at ~5s per attempt;
Retry-Afteris honored up to 30s.