Docs

Python

Python SDK

whire is the async Python client for the whole API: typed responses, automatic retries, and idempotency keys on every write, so a retried call never makes two records.

Install#

Bash
pip install "whire>=0.2"

Python 3.11 or newer. It depends only on httpx and pydantic v2, and ships type hints.

Create a client#

Python
from whire import WhireClient

# the sandbox: https://sandbox.whire.ai
client = WhireClient(environment="sandbox")

# production: a key is required
client = WhireClient(api_key="…", environment="production")

# your own deployment
client = WhireClient(base_url="https://payouts.example.com", api_key="…")

The key can also come from WHIRE_API_KEY, and the target from WHIRE_BASE_URL or WHIRE_ENVIRONMENT. It is sent as X-API-Key. Use async with WhireClient(...) as client: or call await client.close() when you are done.

Every method is async. Arguments are keyword-only except the leading record id. Responses are pydantic models with snake_case attributes (payer.payer_id), Decimal money and timezone-aware datetimes.

Start every integration by asking what the deployment can do:

Python
caps = await client.capabilities()
print(caps.settlement, caps.simulated)   # True True on the sandbox

settlement says whether it moves money at all, simulated whether that money is simulated.

What is where#

Namespace Methods
client.payers create, activate, suspend, add_funding_source, set_default_funding_source, get, list
client.users create, add_payment_method, set_default_payment_method, create_beneficiary, get, list
client.beneficiaries create, get, validate_iban
client.mandates create, validate, revoke, get, list
client.authorizations create, verify
client.payouts create, submit, confirm, execute, get, list, record_event, wait, wait_for_kyc, wait_for_settlement
client.simulation get, reset
client.x402 supported, verify, settle, quote, pay
client.mcp the MCP endpoint from the same client

Each call maps to a REST endpoint; the field names are the same in snake_case. The Quickstart walks the first five.

The payout lifecycle#

draft → pending_kyc → approved → processing → paid → returned
                   ↘ kyc_rejected         ↘ failed

create makes a draft under a mandate, submit sends it to KYC review, execute sends the money. On a deployment with a rail the KYC decision arrives by itself, so you poll for it. There are no webhooks.

Python
payout = await client.payouts.create(
    beneficiary_id=payee.beneficiary_id,
    mandate_id=mandate.mandate_id,
    amount="20.00",  # Decimal, int, float or str; positive, two decimals at most
    reason="Invoice 2026-091",
)
await client.payouts.submit(payout.payout_id)

payout = await client.payouts.wait_for_kyc(payout.payout_id, timeout=30)
if payout.status == "kyc_rejected":          # final: create a new draft
    raise SystemExit(payout.last_note)

execution = await client.payouts.execute(payout.payout_id)
if execution.status == "processing":  # accepted; settlement is reported later
    payout = await client.payouts.wait_for_settlement(
        payout.payout_id, timeout=60
    )

wait_for_kyc polls until the payout leaves pending_kyc; wait_for_settlement until it leaves processing. Both raise WhireTimeoutError on expiry, carrying the last payout read. payouts.wait(id, until=lambda p: ...) is the general form.

confirm is an optional human confirmation between approved and execute. It settles nothing.

Execute outcomes#

Never resend

A payout is executed at most once. If the rail gives no answer, the payment may or may not have gone out, and a retry is refused until the rail reports.

payouts.execute() returns a PayoutExecution or raises PayoutExecutionRefused.

Outcome Meaning Do
status == "paid", provider_reference set Settled. Nothing.
status == "processing" Accepted, settlement pending. wait_for_settlement()
status == "processing" and unresolved The rail did not confirm. Do not resend. wait_for_settlement() until it reports paid or failed.
PayoutExecutionRefused, execution_refused Refused before sending, or rejected by the rail (failed). Read the payout, then ask a human.
PayoutExecutionRefused, already_executed Already sent. Nothing. Never create a replacement automatically.
Python
from whire import PayoutExecutionRefused

try:
    execution = await client.payouts.execute(payout.payout_id)
except PayoutExecutionRefused as e:
    # failed, or unchanged with a note in its history
    payout = await client.payouts.get(payout.payout_id)
    print(e.error_code, e, payout.status)

Idempotency and retries#

The SDK sends a fresh Idempotency-Key on every POST and reuses it, with the same body, on every retry of that call. For money-moving calls, pin the key yourself and store it with the payout id:

Python
import uuid
from whire import AmbiguousResponseError, NetworkError

key = str(uuid.uuid4())
try:
    execution = await client.payouts.execute(payout.payout_id, idempotency_key=key)
except AmbiguousResponseError:
    raise  # it may have been processed: read the payout first
except NetworkError as e:
    # it never arrived; repeating it with the same key is safe
    execution = await client.payouts.execute(
        payout.payout_id, idempotency_key=e.idempotency_key
    )

The same key and body again replays the stored answer (.replayed is True); the same key with a different body is 422. Keys live 24 hours; see Idempotency.

Reads, keyed writes and the other safe calls are retried on network errors, 429, 409 and 5xx with jittered backoff (max_retries=3). A write that may have reached the server is never retried blindly: a read timeout raises AmbiguousResponseError straight away.

Errors#

Every exception derives from WhireError. str(e) is the server's sentence and e.to_agent_dict() is a JSON-safe dict an agent can act on: error, error_code, status_code, retryable, needs_user_action, is_input_error, suggestion.

Exception When Retried
AuthenticationError 401: missing or wrong key no
BadRequestError 400: the message says what to change no
NotFoundError unknown record (400) or route (404) no
PayoutExecutionRefused execute refused no
IdempotencyMismatchError 422: key reused with another body no
RateLimitError, ServerError 429, 5xx yes
NetworkError the request never arrived yes
AmbiguousResponseError it may have been processed no
InvalidInputError rejected locally; nothing was sent no
WhireTimeoutError a wait_* call ran out of time no

An unknown record id is a 400 with the sentence "Payout abc not found."; a 404 means the route does not exist, almost always a wrong base_url.

Next#