paybondpaybond
Sign in

Python SDK reference

Public Paybond Kit Python API surface — async sessions, intents, Signal reads, capability verification, and typed error classes — cross-linked to the Harbor OpenAPI contract.

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-bound HarborClient. Use for operations the high-level intents helper does not wrap (for example verify_capability).
  • paybond.intents — see PaybondIntents.
  • 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 a finally block or async 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/tip
  • get_ledger_authority()GET /ledger/v1/authority
  • get_ledger_events(after_seq=..., limit=...)GET /ledger/v1/events
  • get_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: str

Binds (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 | None

Evidence response

paybond.intents.submit_evidence returns a plain dict[str, Any] with at least the following keys:

KeyTypeMeaning
intent_idstr (UUID)Harbor-echoed intent id; compared against the request.
tenantstrHarbor-echoed tenant realm.
statestrNew intent state (for example "evidence_submitted").
predicate_passedbool (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 | None

Raised 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: str

Raised 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 returns 409.
  • Log tenant_id, intent_id, status_code, url, correlation id (when present), and a redacted body_text. Never log the API key, Harbor JWT, or capability token.
  • Backoff is exponential with jitter, bounded at ~5s per attempt; Retry-After is honored up to 30s.