paybondpaybond
Sign in

Python quickstart

Start with Paybond's one-command sandbox guardrail scaffold, then open a Kit session in Python for production intent, capability, and evidence flows.

Sandbox agent middleware? Start with Agent quickstart — login, CLI smoke, and paybond.agent() with no signing keys. This page covers the full production intent lifecycle.

Plan choice: sandbox integration = Free Developer; low-volume production = Pay as you go.

For a first paid-tool guardrail integration, start with the sandbox scaffold:

paybond-kit-init \
  --preset paid-tool-guard \
  --framework provider-agnostic \
  --out paybond_paid_tool_guard.py

This Free Developer path is sandbox-only. It bootstraps a sandbox guardrail intent, wraps your paid-tool handler, submits simulator evidence, and does not generate a paid-tool implementation. See One-command guardrails for the focused walkthrough.

For multi-tool agents, prefer paybond.instrument() — loads policy and returns deferred tool shells by default; bind per session with .bind() or pass a context provider for request-scoped binding. See Agent middleware. For the fastest sandbox path, use paybond.agent() (opt-in sandbox bootstrap).

import asyncio
import os

from paybond_kit import Paybond


async def main() -> None:
    paybond = await Paybond.open(
        api_key=os.environ["PAYBOND_API_KEY"],
        expected_environment="sandbox",
    )
    try:
        result = await paybond.agent(
            policy="./paybond.policy.yaml",  # or preset id "travel"
            tools={"premium_search": paid_provider.search},
        )
        # result.tools — guarded sandbox tools; register with your agent runtime.
        # Production: paybond.instrument(...) then await instrumented.bind(...) per session.
    finally:
        await paybond.aclose()


asyncio.run(main())

For an existing bound run, use paybond.wrap_tools(run, tools). Per-tool guard_tool remains available for single-handler integrations — see Agent integrations.

This quickstart shows the standard Paybond application flow in Python:

  1. Open an authenticated session against the Gateway with a service-account API key.
  2. Create a principal-signed intent (or bootstrap a sandbox guardrail for the first path).
  3. Read the funded intent's capability_token from create, fund, or bootstrap — see Capabilities.
  4. Verify that capability before tool work with paybond.spend_guard(intent_id, capability_token).
  5. Submit signed evidence so Harbor can evaluate the predicate and transition the intent.

Every call below is tenant-scoped. The tenant comes from the Gateway exchange and is bound to the returned client, so you never pass a tenant ID by hand.

allowed_tools should contain your application's own operation names. Paybond does not define that catalog; Harbor simply checks that the later authorize_spend(operation=...) or verify_capability(operation=...) call matches one of the names attached to the intent.

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

Prerequisites

  • Python 3.11+.
  • A paybond_sk_sandbox_… or paybond_sk_live_… service-account API key issued for your tenant realm. For local sandbox work, run paybond-kit-login; live production keys are created in Console by tenant admins and stored in your deployment secret manager.
  • 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.

Install

Install the published package:

pip install paybond-kit

Optional extras:

pip install "paybond-kit[langgraph]"
pip install "paybond-kit[openai-agents]"
pip install "paybond-kit[crewai]"
pip install "paybond-kit[mcp]"
pip install "paybond-kit[langgraph,mcp]"

Install only the extras your runtime needs:

ExtraEnables
langgraphLangGraph hook dependencies (langchain-core, langgraph) and paybond agent demo langgraph smoke
mcpPackaged stdio MCP server (paybond-mcp-server) and paybond agent demo mcp smoke
claude-agentsClaude Agents SDK and paybond agent demo claude-agents smoke
openai-agentsOpenAI Agents SDK and paybond agent demo openai-agents smoke
crewaiCrewAI tool guards and paybond agent demo crewai smoke
pydantic-aiPydantic AI tool guards and paybond agent demo pydantic-ai smoke
google-adkGoogle ADK FunctionTool guards and paybond agent demo google-adk smoke

Combine extras when needed: pip install "paybond-kit[langgraph,mcp]".

pipx (global CLI)

The PyPI package is paybond-kit; the CLI executable is paybond. On zsh, always quote specs that include [extras] or the shell reports no matches found.

# Fresh install with an optional extra
pipx install 'paybond-kit[langgraph]'

# Already have base paybond-kit — add LangGraph deps without reinstalling
pipx inject paybond-kit langgraph langchain-core

# Or recreate the venv with the extra
pipx install --force 'paybond-kit[langgraph]'

# One-shot run (no global install): --spec is for pipx run only; pass paybond explicitly
pipx run --spec 'paybond-kit[langgraph]' paybond agent demo langgraph smoke \
  --operation paid-tool \
  --requested-spend-cents 100 \
  --evidence-preset cost_and_completion \
  --format json

Plain pipx run --spec paybond-kit paybond … installs the base package only and does not include optional extras.

Published wheels bundle the native signing extension used by the high-level intent helpers. For a fuller runnable application, see the Python example projects.

Sandbox login

paybond-kit-login

The login CLI starts a sandbox device approval flow, writes PAYBOND_API_KEY to .env.local with file mode 0600, adds the default .env.local target to .gitignore when needed, and refuses to overwrite an existing key unless --force is passed. Custom env-file paths inside a git repo must already be ignored. It prints the masked key identity and target sandbox tenant only; it never prints the raw key after writing.

Authenticated session

Paybond.open resolves the tenant realm from the hosted Gateway and returns tenant-bound Gateway, Harbor, Signal, fraud, A2A, and protocol clients. The examples below assume a sandbox key; set expected_environment to "live" when using a paybond_sk_live_… key. The paybond.harbor property is created by Paybond.open(...); use paybond.spend_guard(...) for normal tool guards and paybond.harbor directly only for lower-level Harbor calls.

import asyncio
import os

from paybond_kit import Paybond


async def main() -> None:
    paybond = await Paybond.open(
        api_key=os.environ["PAYBOND_API_KEY"],
        expected_environment="sandbox",
    )
    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.
  • Gateway is the public surface. Harbor operations go through Gateway's tenant-scoped /harbor and /verify routes.
  • 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.

Capability token source

Normal integrations do not supply capability tokens through environment variables. The token comes from the intent lifecycle. See Capabilities for the full reference, agent-run lifecycle, and incorrect integration patterns.

PathSDK callWhen you get capability_token
Sandbox first integrationpaybond.guardrails.bootstrap_sandbox(...)Immediately in the bootstrap response
Production immediate fundpaybond.intents.create(...)When the selected rail funds in the create response
Production delayed / x402paybond.intents.fund(...)After funding succeeds

Additional notes:

  • paybond.spend_guard(...).authorize_spend(...) and lower-level verify_capability calls go through Gateway POST /verify. A 200 response with allow: False is a business denial, not an HTTP transport error.
  • paybond.intents.submit_evidence signs the payee evidence binding and posts it to Gateway POST /harbor/intents/{id}/evidence. Reuse the same idempotency_key when retrying the same submission.
  • The client validates tenant and intent echoes on protected reads and writes so mismatched responses fail closed.
  • If you do not pass intent_id to create, read the generated id from created["intent_id"].
  • There is no POST /v1/capabilities/issue route. Do not use os.environ["CAPABILITY_TOKEN"] in production agent runs.
  • 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.

Recognition proofs

Every Gateway-backed intent mutation in these snippets takes a recognition_proof. Think of it as a short-lived signature that says: "this tenant-registered agent key is authorizing this exact Gateway request right now."

Paybond does not create or hand this proof to your app, and Kit does not generate it automatically. The proof is created by your trusted backend, KMS-backed signer, wallet service, or agent runner:

  1. A tenant admin registers the agent runtime's Ed25519 public key in Paybond's trusted agent key registry and assigns it a stable key_id.
  2. Your trusted component keeps the matching Ed25519 private key. Do not expose that key to a browser, model prompt, or untrusted tool.
  3. Immediately before a protected mutation, that component signs a fresh AgentRecognitionProofV1 for the exact request.
  4. Kit base64url-encodes the finished proof object and sends it as x-paybond-agent-recognition-proof.
  5. Gateway verifies the signature against the registered public key, checks tenant/purpose/request binding, and rejects replayed nonces.

A typical app keeps this behind an internal helper or endpoint:

async def issue_agent_recognition_proof_v1(
    *,
    purpose: str,
    method: str,
    path: str,
    body: dict[str, object],
) -> AgentRecognitionProofV1: ...

Generate a fresh proof after the request body is fixed:

SDK callPurposeRequest envelope
paybond.intents.create(...)harbor.intent.createPOST /harbor/intents, SHA-256 digest of the signed create body
paybond.intents.fund(...)harbor.intent.fundPOST /harbor/intents/{intent_id}/fund, SHA-256 digest of the empty object body
paybond.intents.submit_evidence(...)harbor.intent.evidence.submitPOST /harbor/intents/{intent_id}/evidence, SHA-256 digest of the signed evidence body

Because the proof covers the body digest, the proof issuer must run where it can see or reproduce the final request body. If you prebuild Harbor bodies outside the high-level helpers, send them through the lower-level Gateway Harbor methods such as paybond.harbor.create_intent(...) and paybond.harbor.submit_evidence(...).

Each proof should include schema_version=1, kind="paybond.agent_recognition_proof_v1", the tenant-registered key_id, signature_algorithm="ed25519-sha256-json-v1", verifier_context.tenant_id=paybond.harbor.tenant_id, verifier_context.verifier_id="paybond-gateway", a request envelope, a unique nonce, a short issued_at / expires_at window of 10 minutes or less, and the Ed25519 digest/signature fields.

Only PAYBOND_API_KEY is Paybond-issued in these examples. The APP_* environment variables below are local sandbox/live quick-start placeholders for values owned by your application, such as signing seeds, rail choices, wallet signatures, and proof JSON returned by your signer. They are not static production config. Production code should call your proof issuer for each create, fund, fund retry, and submit_evidence request.

Agent framework integrations

Use paybond.instrument() for multi-tool agents — deferred by default; use sandbox: true or paybond.agent() for local sandbox. Bind per session with bind() or a lazy context provider. For LangGraph, pass framework="langgraph"; for custom orchestrators, omit framework (generic).

See Agent integrations for runtime-specific patterns (LangGraph, MCP, Vercel AI) and Agent middleware for registry and bind when you need step-by-step control.

Principal-signed intent create

paybond.intents.create builds a principal-signed intent body and submits it through Gateway POST /harbor/intents. intent_id can default to uuid4() when omitted, but this quickstart passes it explicitly so the recognition proof can bind to a stable request.

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

from paybond_kit import Paybond


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

    paybond = await Paybond.open(
        api_key=os.environ["PAYBOND_API_KEY"],
        expected_environment="sandbox",
    )
    try:
        intent_id = uuid4()
        create_recognition_proof = json.loads(os.environ["APP_INTENT_CREATE_RECOGNITION_PROOF_JSON"])
        created = await paybond.intents.create(
            principal_did="did:web:example.com#principal",
            principal_signing_seed=principal_seed,
            recognition_proof=create_recognition_proof,
            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"],
            settlement_rail="stripe_connect",
            intent_id=intent_id,
            idempotency_key=f"intent:{intent_id}",
        )
        created_intent_id = UUID(str(created["intent_id"]))
        if created_intent_id != intent_id:
            raise RuntimeError(f"intent mismatch: requested={intent_id} gateway={created_intent_id}")

        capability_token = str(created.get("capability_token") or "")
        if not capability_token:
            raise RuntimeError("intent created without capability_token; fund the intent before verifying tools")

        guard = paybond.spend_guard(intent_id, capability_token)
        verified = await guard.authorize_spend(
            operation="travel.booker.purchase",
            requested_spend_cents=18_700,
        )
        if not verified.allow:
            raise RuntimeError(
                f"verify denied: {verified.code or 'deny'} {verified.message or ''}".strip()
            )

        # Only run the real action after Paybond authorizes the agent to do it.

        submitted_at_rfc3339 = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        evidence_recognition_proof = json.loads(os.environ["APP_EVIDENCE_RECOGNITION_PROOF_JSON"])
        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,
            recognition_proof=evidence_recognition_proof,
            submitted_at_rfc3339=submitted_at_rfc3339,
            idempotency_key=f"evidence:{intent_id}",
        )
    finally:
        await paybond.aclose()


asyncio.run(main())

Choose intent_id, submitted_at_rfc3339, and idempotency keys before issuing request-bound recognition proofs so your signer can bind to the same request the SDK will send.

Managed-policy intent creation is not wrapped by this helper. If you need registry-bound signing, call paybond.harbor.create_intent with a pre-built body and use the Harbor 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_rail is required and principal-signed. It requests one allowed rail such as stripe_connect, stripe_ach_debit, stripe_mpp, adyen_manual_capture, or x402_usdc_base; you are not supplying a Stripe destination, MPP profile id, Adyen merchant credentials, or x402 receive address. Harbor resolves the destination from the tenant's canonical settlement config and snapshots it onto the intent. For stripe_mpp, tenant admins configure the MPP profile (and optional Tempo session settings) in Configure settlement rails before enabling the rail. For adyen_manual_capture, configure the BYO Adyen destination in Configure Adyen settlement and confirm with paybond adyen ready / paybond adyen doctor.

Intent pricing remains USD-denominated even on the stablecoin rail: keep budget["currency"] and currency as "usd" with cent-based amounts, while x402_usdc_base tells Harbor to settle that value in USDC on Base.

Sandbox → production

After sandbox guardrails or agent middleware work locally, move to production intents with policy_binding (signing v7). The flow:

  1. Scaffold policypaybond policy init --preset travel writes paybond.policy.yaml with intent.policy_binding.template_id aligned to your completion preset.
  2. Publish template head — In Console or via paybond policy templates and preview, publish the managed template head for your tenant. Record template_id, version_seq, materialized_predicate, and policy_content_digest_hex.
  3. Create intent — Use PaybondPolicy.to_intent_create_input() and paybond.intents.create_with_policy_binding() so Harbor binds the published predicate at create time.
  4. Fund and attach — Fund if the rail requires it, then bind agent middleware with production_evidence (see Production attach).
from paybond_kit import Paybond
from paybond_kit.policy import PaybondPolicy

paybond = await Paybond.open(
    api_key=os.environ["PAYBOND_API_KEY"],
    expected_environment="live",
)
try:
    policy = await PaybondPolicy.load("./paybond.policy.yaml")
    intent_input = policy.to_intent_create_input(
        principal_did=os.environ["APP_PRINCIPAL_DID"],
        principal_signing_seed=principal_seed,
        payee_did=os.environ["APP_PAYEE_DID"],
        payee_signing_seed=payee_seed,
        deadline_rfc3339="2030-12-31T23:59:59Z",
        settlement_rail="stripe_connect",
        recognition_proof=await issue_agent_recognition_proof_v1(...),
        materialized_predicate=published_head["materialized_predicate"],
        policy_template_id=published_head["template_id"],
        policy_version_seq=published_head["version_seq"],
        policy_content_digest_hex=published_head["digest_hex"],
    )
    created = await paybond.intents.create_with_policy_binding(**intent_input.__dict__)
    # Fund if needed, then attach middleware or call spend_guard and submit_evidence.
finally:
    await paybond.aclose()

Init scaffolds (paybond-kit-init --preset agent-middleware, paybond init completion, and paybond init wizard stubs) include a commented production block with the same pattern. For the sandbox-only first path, see One-command guardrails.

Settlement rail and completion preset

Choose your settlement rail first (stripe_connect, stripe_ach_debit, stripe_mpp, adyen_manual_capture, or x402_usdc_base on settlement_rail), then pick a matching completion preset for tool-completion evidence. Funding webhooks and /fund authorization move the intent to funded; presets describe what to submit after you hold a capability_token — never Stripe PaymentIntent ids or x402 payment-session ids as evidence.

# After rail choice — scaffold aligned evidence helpers
paybond init completion --preset ach_paid_api_ok          # stripe_ach_debit
paybond init completion --preset ach_travel_booking       # travel on ACH
paybond init completion --preset x402_travel_booking        # travel on x402
paybond init completion --preset x402_saas_api_purchase   # SaaS seat/API on x402
paybond init completion --preset x402_delivery_receipt      # x402 signed receipt
paybond init completion --preset invoice_payment_confirmed  # AP/invoice webhook (any rail)

See Funding vs completion for the full rail table and checklists.

Stablecoin funding via /fund

For intents created on the x402_usdc_base rail, Harbor returns a funding object with payment-session metadata and the tenant-scoped harbor_fund_endpoint. Use paybond.intents.fund_with_x402 for the full handshake (402 challenge → sign → retry → poll until capability_token).

funded = await paybond.intents.fund_with_x402(
    intent_id,
    recognition_proof=await issue_agent_recognition_proof_v1(
        purpose="harbor.intent.fund",
        method="POST",
        path=f"/harbor/intents/{intent_id}/fund",
        body={},
    ),
    sign_payment=x402_wallet.sign_payment,
    issue_recognition_proof=lambda envelope: issue_agent_recognition_proof_v1(
        purpose="harbor.intent.fund",
        method=envelope["method"],
        path=envelope["path"],
        body=envelope["body"],
    ),
)
print(funded.status_code, funded.funded, funded.capability_token)

sign_payment and issue_recognition_proof stay app-owned — Kit never holds x402 wallet keys. Each /fund call needs a fresh AgentRecognitionProofV1 (single-use nonces).

Local x402 contract testing

ModeCommandWhat it exercises
Offline (no Docker)paybond dev loop --offlineIn-process Gateway mock with x402 fund state machine on POST /harbor/intents/{id}/fund
WireMock (Docker)paybond dev up then --gateway http://127.0.0.1:18089Scenario mappings 1113: 402 → 202 → 200 for intent aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa

Point Paybond.open(..., gateway=...) or paybond --gateway ... intents fund ... at the mock Gateway to rehearse fund_with_x402 without a remote sandbox API.

MPP funding via /fund

For intents created on the stripe_mpp rail, Harbor returns WWW-Authenticate: Payment … challenges on 402. Use the helper that matches your funding mode:

ModeHelperChallenge
One-shot Stripe chargepaybond.intents.fund_with_mpp_chargeintent="charge", method="stripe"
Tempo session channelpaybond.intents.fund_with_mpp_sessionintent="session", method="tempo"
funded = await paybond.intents.fund_with_mpp_charge(
    intent_id,
    recognition_proof=await issue_agent_recognition_proof_v1(
        purpose="harbor.intent.fund",
        method="POST",
        path=f"/harbor/intents/{intent_id}/fund",
        body={},
    ),
    create_payment_credential=mpp_wallet.create_payment_credential,
    issue_recognition_proof=lambda envelope: issue_agent_recognition_proof_v1(
        purpose="harbor.intent.fund",
        method=envelope["method"],
        path=envelope["path"],
        body=envelope["body"],
    ),
)
print(funded.status_code, funded.funded, funded.capability_token)
# funded.funding.settlement_asset, deposit_amount_base_units (create bootstrap)
# funded.funding.payment_auth_intent, payment_auth_method, challenge_id (after 402)
# funded.funding.channel_id, session_protocol, channel_status (session mode)

create_payment_credential and issue_recognition_proof stay app-owned — Kit never holds MPP wallet or SPT secrets. Kit retries with payment_authorization, sent as x-paybond-payment-authorization: Payment … so tenant Authorization: Bearer stays available.

For streaming session budgets, swap in fund_with_mpp_session when the intent needs a Tempo channel deposit instead of a one-shot charge.

Sandbox note: When Tempo session funding is configured in sandbox, the first fund_with_mpp_charge call may receive both charge and session WWW-Authenticate headers. The charge helper selects the intent="charge" challenge automatically; use fund_with_mpp_session when you intend to open a Tempo channel.

Advanced: manual /fund loop

Use paybond.intents.fund directly when you need fine-grained control over each HTTP status or idempotency key.

The snippet below uses two app-owned helpers: issue_agent_recognition_proof_v1(...) creates a fresh request-bound proof, and x402_wallet.sign_payment(...) signs the x402 challenge returned by Harbor.

fund_recognition_proof = await issue_agent_recognition_proof_v1(
    purpose="harbor.intent.fund",
    method="POST",
    path=f"/harbor/intents/{intent_id}/fund",
    body={},
)
first = await paybond.intents.fund(intent_id, recognition_proof=fund_recognition_proof)
if first.status_code == 402:
    if not first.payment_required:
        raise RuntimeError("missing PAYMENT-REQUIRED challenge")

    payment_signature = await x402_wallet.sign_payment(first.payment_required)
    retry_recognition_proof = await issue_agent_recognition_proof_v1(
        purpose="harbor.intent.fund",
        method="POST",
        path=f"/harbor/intents/{intent_id}/fund",
        body={},
    )

    funded = await paybond.intents.fund(
        intent_id,
        recognition_proof=retry_recognition_proof,
        payment_signature=payment_signature,
    )
    if not funded.capability_token:
        raise RuntimeError("funding is pending; poll /fund with a fresh recognition proof before guarding tools")
    print(funded.status_code, funded.funded, funded.capability_token)
elif first.capability_token:
    print(first.status_code, first.funded, first.capability_token)
else:
    raise RuntimeError("intent is not funded yet; poll /fund with a fresh recognition proof before guarding tools")

Notes (manual loop):

  • issue_agent_recognition_proof_v1(...) is your trusted backend or agent-runner signer, not a Kit helper. Local sandbox/live quick-start scripts may load APP_FUND_RECOGNITION_PROOF_JSON and APP_FUND_RETRY_RECOGNITION_PROOF_JSON from the environment, but production code should issue a fresh AgentRecognitionProofV1 for each /fund call.
  • Generate the AgentRecognitionProofV1 with purpose="harbor.intent.fund", verifier_context.tenant_id=paybond.harbor.tenant_id, verifier_context.verifier_id="paybond-gateway", and a request_envelope for POST /harbor/intents/{intent_id}/fund whose body digest is the SHA-256 of the exact JSON body Kit sends ({} for this helper).
  • Use a unique nonce, short issued_at/expires_at window, and the Ed25519 private key for a tenant-registered agent key_id; the signed proof includes message_digest_sha256_hex, signing_public_key_ed25519_hex, and ed25519_signature_hex.
  • The retry needs its own recognition proof because proof nonces are single-use. payment_signature is separate: it comes from your x402 wallet or facilitator after signing payment_required, and Kit sends it as the payment-signature header. If a local quick-start script uses APP_X402_PAYMENT_SIGNATURE, treat it as a stand-in for that signer output.
  • paybond.intents.fund calls Gateway POST /harbor/intents/{id}/fund.
  • 402 is a normal funding challenge, not an exception. The helper returns payment_required so your integration can call an x402-capable signer.
  • 202 means authorization is pending; poll /fund with a fresh recognition proof before guarding tools. 200 means Harbor transitioned the intent to funded.

Advanced troubleshooting

Only use direct token verification when support asks you to inspect an existing funded intent. Keep this out of production app configuration; normal integrations should read the capability token from create or fund.

export PAYBOND_INTENT_ID="00000000-0000-0000-0000-000000000000"
export PAYBOND_CAPABILITY="paybond_cap_..."
paybond = await Paybond.open(
    api_key=os.environ["PAYBOND_API_KEY"],
    expected_environment="sandbox",
)
try:
    verified = await paybond.harbor.verify_capability(
        intent_id=UUID(os.environ["PAYBOND_INTENT_ID"]),
        token=os.environ["PAYBOND_CAPABILITY"],
        operation="travel.booker.purchase",
        requested_spend_cents=18_700,
    )
    print(verified.allow, verified.code, verified.message)
finally:
    await paybond.aclose()

Settlement confirmation

After evidence is submitted and the predicate passes, confirm settlement in-process:

await paybond.intents.confirm_settlement(
    intent_id,
    body={},  # Harbor derives release/refund from the recorded predicate result.
    recognition_proof=recognition_proof,
)

For protocol/A2A automation use paybond.protocol.confirm_harbor_settlement; for human-reviewed or disputed settlement use the Operator Console. See the Harbor API reference.

Error handling

Kit symbolRaised whenNotes
GatewayAuthErrorGateway rejected the API key or omitted tenant_id.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 a critical tenant-isolation error; do not retry.

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

Further reading