paybondpaybond
Sign in

Python quickstart

Open a service-account Paybond Kit session in Python, verify capabilities, and submit signed evidence against Harbor.

Python quickstart

This quickstart walks through the canonical Paybond Kit flow in Python:

  1. Open an authenticated session against the Gateway with a service-account API key.
  2. Verify a Biscuit capability token scoped to a funded intent.
  3. Submit signed evidence so Harbor can evaluate the predicate and transition the intent.

Every call below is tenant-scoped: the realm is derived from the Gateway exchange and bound to the returned client. You never pass a tenant id by hand, and the Kit will fail closed if Harbor ever echoes a different tenant or intent than you requested.

allowed_tools should contain your application's own tool or operation names. Paybond does not define a fixed catalog here; Harbor simply enforces that the later verify_capability(..., operation=...) call matches one of the names you bound onto the intent.

For the full API surface, see the Python SDK reference.

Prerequisites

  • Python 3.11+.
  • A paybond_sk_… service-account API key issued for your tenant realm.
  • The Gateway and Harbor base URLs reachable from your runtime (for example https://gateway.example.com, https://harbor.example.com).
  • For the principal-signed intent create flow: a 32-byte Ed25519 seed for the principal and a 32-byte seed for the payee bound to the intent. Intent creation and evidence signing also require the bundled native extension built (see Install).

Install

Install the published package:

pip install paybond-kit

Optional extras:

pip install "paybond-kit[agents,langgraph]"

Published wheels bundle the paybond_kit._native extension used for principal-side intent signing and payee evidence signing.

For local development from a checkout:

cd kit/python
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
maturin develop

maturin develop is only required for source builds from this repository. Without the bundled native extension, paybond.intents.create and paybond.intents.submit_evidence raise ImportError at call time.

Authenticated session

Paybond.open exchanges your service-account API key for a short-lived Harbor JWT at the Gateway (POST /v1/auth/harbor-access), caches the tenant realm echoed by the response, and returns a tenant-bound async Harbor client.

import asyncio
import os

from paybond_kit import Paybond


async def main() -> None:
    paybond = await Paybond.open(
        gateway_base_url="https://gateway.example.com",
        api_key=os.environ["PAYBOND_API_KEY"],
        harbor_base_url="https://harbor.example.com",
    )
    try:
        print("tenant realm:", paybond.harbor.tenant_id)
    finally:
        await paybond.aclose()


asyncio.run(main())

Key properties of the session:

  • Tenant is derived, not supplied. The tenant realm is whatever the Gateway echoes. Do not read the tenant from request bodies or environment variables at call time.
  • Token rotation is automatic. Each Harbor call awaits a cached JWT and refreshes before expiry. Force rotation with await paybond.rotate_harbor_token() during a credential rotation drill.
  • Sessions are single-tenant. Construct one Paybond instance per (tenant, service account). Never share across tenants or reuse across concurrent agent runs bound to different intents.
  • Release resources. Always await paybond.aclose() so the underlying httpx.AsyncClient drains. Paybond.open raises GatewayAuthError on any gateway or token failure.

Verify and submit evidence walkthrough

Once you have a funded intent_id and a Biscuit capability token minted for the payee, the tenant-safe integration pattern is: verify the capability, run the tool work, then submit signed evidence.

import asyncio
import os
from datetime import datetime, timezone
from uuid import UUID

from paybond_kit import Paybond, PaybondCapabilityBinding


async def main() -> None:
    paybond = await Paybond.open(
        gateway_base_url="https://gateway.example.com",
        api_key=os.environ["PAYBOND_API_KEY"],
        harbor_base_url="https://harbor.example.com",
    )
    try:
        intent_id = UUID(os.environ["PAYBOND_INTENT_ID"])
        cap_token = os.environ["PAYBOND_CAPABILITY"]
        payee_seed = bytes.fromhex(os.environ["PAYBOND_PAYEE_SEED_HEX"])  # 32 bytes

        binding = PaybondCapabilityBinding(
            harbor=paybond.harbor,
            intent_id=intent_id,
            capability_token=cap_token,
        )

        verified = await binding.harbor.verify_capability(
            intent_id=binding.intent_id,
            token=binding.capability_token,
            operation="payments.capture",
            requested_spend_cents=18_700,
        )
        if not verified.allow:
            raise RuntimeError(
                f"verify denied: {verified.code or 'deny'} {verified.message or ''}".strip()
            )

        # …run the tool work guarded by the verified capability here…

        submitted_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        result = await paybond.intents.submit_evidence(
            intent_id,
            {"flight_booked": True, "price": 187, "confirmation": "PNR-4GH271"},
            payee_did="did:web:example.com#booker-agent",
            payee_signing_seed=payee_seed,
            submitted_at_rfc3339=submitted_at,
            idempotency_key=f"evidence:{intent_id}:{submitted_at}",
        )
        print("evidence recorded:", result["state"], result.get("predicate_passed"))
    finally:
        await paybond.aclose()


asyncio.run(main())

Notes:

  • verify_capability maps to Harbor POST /verify (operation id harborVerifyCapability). A 200 response with allow: False is not an HTTP error — treat code and message as the deny reason.
  • paybond.intents.submit_evidence signs the payee binding with canonical JSON rules and POSTs it to Harbor POST /intents/{id}/evidence (operation id harborSubmitEvidence). Reuse the same idempotency_key to safely retry a transient 5xx; changing the body under the same key will fail with 409.
  • Harbor enforces tenant and intent echo on every response; the Kit raises TenantBindingError on mismatches so confused-deputy bugs fail closed.
  • Keep payee_signing_seed in a secret manager. The value must be exactly 32 bytes and must correspond to the payee key bound on the intent; any drift will be rejected server-side.

Agent framework integrations

Paybond ships first-party hooks for the OpenAI Agents SDK and LangGraph that wrap the same PaybondCapabilityBinding shown above, so you do not have to re-implement the verify/submit loop inside tool bodies.

from paybond_kit.agents_sdk import paybond_capability_input_guardrail
from paybond_kit.langgraph_hooks import paybond_awrap_tool_call_capability
from langgraph.prebuilt import ToolNode

guard = paybond_capability_input_guardrail()
# Attach `guard` to OpenAI Agents tools and pass `context=binding` into Runner.run(...).

tool_node = ToolNode(
    tools=[],  # your tools
    awrap_tool_call=paybond_awrap_tool_call_capability(binding),
)

See Agent integrations for end-to-end examples, including the LangGraph runnable sketch shipped under examples/paybond-kit-langgraph-python/.

Principal-signed intent create

paybond.intents.create wraps the principal-side signing dance: it builds a canonical CreateIntentRequest body, signs it with the principal's 32-byte Ed25519 seed, and POSTs it to Harbor (operation id harborCreateIntent).

import asyncio
import os
from uuid import UUID, uuid4

from paybond_kit import Paybond


async def main() -> None:
    principal_seed = bytes.fromhex(os.environ["PAYBOND_PRINCIPAL_SEED_HEX"])  # 32 bytes
    payee_seed = bytes.fromhex(os.environ["PAYBOND_PAYEE_SEED_HEX"])          # 32 bytes

    paybond = await Paybond.open(
        gateway_base_url="https://gateway.example.com",
        api_key=os.environ["PAYBOND_API_KEY"],
        harbor_base_url="https://harbor.example.com",
    )
    try:
        created = await paybond.intents.create(
            principal_did="did:web:example.com#principal",
            principal_signing_seed=principal_seed,
            payee_did="did:web:example.com#booker-agent",
            budget={"currency": "usd", "max_spend_usd": 200},
            predicate={"version": 1, "root": {"op": "true"}},
            currency="usd",
            amount_cents=20_000,
            evidence_schema={"type": "object"},
            deadline_rfc3339="2030-12-31T23:59:59Z",
            allowed_tools=["travel.planner.plan", "travel.booker.purchase"],
            intent_id=uuid4(),
            idempotency_key=str(uuid4()),
        )
        intent_id = UUID(str(created["intent_id"]))
        await paybond.intents.submit_evidence(
            intent_id,
            {"flight_booked": True, "price": 187, "confirmation": "PNR-4GH271"},
            payee_did="did:web:example.com#booker-agent",
            payee_signing_seed=payee_seed,
        )
    finally:
        await paybond.aclose()


asyncio.run(main())

Managed-policy intent creation (signing_version 3 / policy_binding) is not wrapped by this helper; call paybond.harbor.create_intent with a pre-built body when you need registry-bound signing. See Harbor API — policy registry.

allowed_tools is your namespace, not Paybond's. For example, if your runtime later verifies operation="travel.booker.purchase", that exact string must appear in the intent allow-list.

Settlement confirmation

Settlement confirmation (release / refund) is an operator-tier action and is not wrapped by the Kit today. Confirm through the Operator Console or call Harbor directly: POST /intents/{id}/settlement/confirm (operation id harborConfirmSettlement). See the Harbor API reference.

Error handling

Kit symbolRaised whenNotes
GatewayAuthErrorharbor-access rejected the API key or omitted tenant_id / access_token.status_code and redacted body_text are attached to the instance.
HarborHttpErrorAny Harbor 4xx/5xx after retries are exhausted.Log status_code, url, and a redacted body_text; never log the API key, JWT, or capability token.
TenantBindingErrorHarbor echoed a different tenant or intent than requested.Treat as severity-zero; do not retry.

See the Error handling guide for retry/backoff guidance and logging hygiene.

Further reading