Beta Early access โ€” you're seeing this before public launch. Browse our books See terms of early access

Python SDK

Official Python SDK for the soft.house API โ€” async-first, typed, with retry logic and Pydantic models.

Python SDK โ€” Coming Soon. The soft-house package is not yet published to PyPI โ€” pip install soft-house currently fails. The SDK source lives in the monorepo; the PyPI publish is operator-gated. This page documents the SDK API as it will ship: fully async, Pydantic v2 models, mirroring the TypeScript SDK surface. Until then, integrate using the REST API directly.

Planned Installation

# Not yet published โ€” will fail
pip install soft-house

Requirements: Python 3.10+, httpx, pydantic 2.0+

For development and testing (once published):

# Not yet published โ€” will fail
pip install soft-house[dev]

This installs pytest, pytest-asyncio, pytest-httpx, ruff, and mypy.

Quick Start

The SDK is async-first. All API methods are coroutines:

import asyncio
from soft_house import SoftHouse, WishCreateParams, WishBudget

async def main():
    client = SoftHouse(api_key="sk_test_...")

    # Create a wish
    wish = await client.wishes.create(
        WishCreateParams(
            query="Find me a laptop under $1500",
            budget=WishBudget(max=1500, currency="USD"),
        )
    )

    print(f"Wish created: {wish.id} โ€” {wish.status}")

asyncio.run(main())

Configuration

client = SoftHouse(
    api_key="sk_live_...",              # Required for backend usage
    base_url="https://api.soft.house",  # Default API endpoint
    timeout=30.0,                       # Request timeout in seconds
    max_retries=3,                      # Auto-retry on 5xx and 429
)
ParameterTypeDefaultDescription
api_keystr | NoneNoneAPI key (sk_live_... or sk_test_...)
base_urlstrhttps://api.soft.houseAPI base URL
timeoutfloat30.0Request timeout (seconds)
max_retriesint3Max retry attempts for retryable errors

Wishes API

from soft_house import (
    SoftHouse,
    WishCreateParams,
    WishUpdateParams,
    WishListParams,
    WishBudget,
    WishStatus,
)

client = SoftHouse(api_key="sk_test_...")

# โ”€โ”€ Create a wish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
wish = await client.wishes.create(
    WishCreateParams(
        query="Find me noise-cancelling headphones under $300",
        budget=WishBudget(max=300, currency="USD"),
        protocol="ap2",
        metadata={"category": "electronics"},
    )
)

# โ”€โ”€ Get a wish by ID โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
wish = await client.wishes.get("wish_abc123")
print(wish.status)  # WishStatus.ACTIVE

# โ”€โ”€ Update a wish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
updated = await client.wishes.update(
    "wish_abc123",
    WishUpdateParams(
        query="Find me wireless headphones under $250",
        budget=WishBudget(max=250),
    ),
)

# โ”€โ”€ List wishes with filters โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
result = await client.wishes.list(
    WishListParams(
        status=WishStatus.ACTIVE,
        limit=10,
        offset=0,
        sort="created_at",
        order="desc",
    )
)
for w in result.wishes:
    print(f"{w.id}: {w.query}")
print(f"Total: {result.total}")

# โ”€โ”€ Cancel a wish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
cancelled = await client.wishes.cancel("wish_abc123")
assert cancelled.status == WishStatus.CANCELLED

Mandates API

Mandates authorize payments under the AP2 or ACP protocol:

from soft_house import (
    SoftHouse,
    MandateCreateParams,
    MandateListParams,
    MandateType,
    MandateStatus,
    ProtocolType,
    MerchantInfo,
)

client = SoftHouse(api_key="sk_test_...")

# โ”€โ”€ Create an AP2 mandate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
mandate = await client.mandates.create(
    MandateCreateParams(
        type=MandateType.INTENT,
        protocol_type=ProtocolType.AP2,
        max_amount=1500.00,
        currency="USD",
        merchant_info=MerchantInfo(
            name="Example Electronics",
            domain="electronics.example.com",
            category="electronics",
        ),
    )
)
print(f"Mandate: {mandate.id}, status: {mandate.status}")

# โ”€โ”€ Get a mandate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
mandate = await client.mandates.get("mnd_abc123")

# โ”€โ”€ Verify a mandate's signature โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
verify_result = await client.mandates.verify("mnd_abc123")
print(f"Valid: {verify_result.valid}, Protocol: {verify_result.protocol_type}")

# โ”€โ”€ Revoke a mandate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
revoked = await client.mandates.revoke("mnd_abc123")
assert revoked.status == MandateStatus.REVOKED

# โ”€โ”€ List mandates โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
result = await client.mandates.list(
    MandateListParams(
        protocol_type=ProtocolType.AP2,
        status=MandateStatus.ACTIVE,
        limit=20,
    )
)

Payments API

Process payments against mandates. All financial calculations are server-side (the SDK sends amounts but the server recalculates):

from soft_house import (
    SoftHouse,
    PaymentCreateParams,
    PaymentListParams,
    PaymentStatus,
)
import uuid

client = SoftHouse(api_key="sk_test_...")

# โ”€โ”€ Create a payment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# idempotency_key prevents duplicate charges (required)
payment = await client.payments.create(
    PaymentCreateParams(
        mandate_id="mnd_abc123",
        amount=119.99,
        currency="USD",
        idempotency_key=str(uuid.uuid4()),
        metadata={"order_id": "order_xyz"},
    )
)
print(f"Payment: {payment.id}, status: {payment.status}")

# โ”€โ”€ Get a payment โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
payment = await client.payments.get("pay_abc123")

# โ”€โ”€ List payments โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
result = await client.payments.list(
    PaymentListParams(
        mandate_id="mnd_abc123",
        status=PaymentStatus.SUCCEEDED,
        limit=50,
    )
)
for p in result.payments:
    print(f"{p.id}: {p.amount} {p.currency} โ€” {p.status}")

Idempotency keys are required for all payment operations. The server uses a 24-hour KV cache to prevent duplicate charges. Always generate a unique key per payment attempt.

Error Handling

All errors extend SoftHouseError with structured fields:

from soft_house import (
    SoftHouse,
    SoftHouseError,
    AuthenticationError,
    ForbiddenError,
    NotFoundError,
    ValidationError,
    ConflictError,
    RateLimitError,
    NetworkError,
    WishCreateParams,
)

client = SoftHouse(api_key="sk_test_...")

try:
    wish = await client.wishes.create(
        WishCreateParams(query="Find me a laptop")
    )
except AuthenticationError:
    print("Invalid or missing API key")
except ForbiddenError:
    print("Insufficient permissions")
except ValidationError as e:
    print(f"Invalid input: {e}")
except NotFoundError:
    print("Resource not found")
except ConflictError:
    print("Resource conflict (e.g., duplicate)")
except RateLimitError as e:
    print(f"Rate limited. Retry after: {e.retry_after}s")
except NetworkError:
    print("Network failure (timeout, DNS, etc.)")
except SoftHouseError as e:
    print(f"API error {e.status_code}: {e}")

Error hierarchy

ErrorHTTP StatusRetryableDescription
AuthenticationError401NoInvalid or missing API key
ForbiddenError403NoInsufficient permissions
NotFoundError404NoResource does not exist
ValidationError400NoInvalid request parameters
ConflictError409NoResource conflict
RateLimitError429YesRate limit exceeded
NetworkError-YesNetwork-level failure
SoftHouseError*DependsBase class for all errors

Retry Logic

The SDK automatically retries on:

  • 5xx server errors โ€” exponential backoff with jitter
  • 429 rate limit โ€” respects Retry-After header
  • Network timeouts โ€” exponential backoff
# Customize retry behavior
client = SoftHouse(
    api_key="sk_test_...",
    max_retries=5,   # More retries for critical operations
    timeout=60.0,    # Longer timeout for slow networks
)

Default retry formula: delay = min(initial_delay * 2^(attempt-1), max_delay) + jitter

Type Safety

All models use Pydantic v2 with frozen=True (immutable). The SDK is fully typed and passes mypy --strict:

from soft_house import Wish, WishStatus

# Models are immutable
wish: Wish = await client.wishes.get("wish_abc123")
# wish.status = "cancelled"  # TypeError: frozen instance

# Enum values for compile-time safety
print(WishStatus.ACTIVE)     # "active"
print(ProtocolType.AP2)      # "ap2"
print(MandateType.INTENT)    # "intent"
print(PaymentStatus.SUCCEEDED)  # "succeeded"

Testing

Use pytest-httpx to mock API responses in tests:

import pytest
from soft_house import SoftHouse, WishCreateParams, WishBudget

@pytest.mark.asyncio
async def test_create_wish(httpx_mock):
    httpx_mock.add_response(json={
        "id": "wish_test_1",
        "user_id": "user_1",
        "query": "Test wish",
        "status": "active",
        "budget": {"max": 100, "currency": "USD"},
        "created_at": "2026-01-01T00:00:00Z",
        "updated_at": "2026-01-01T00:00:00Z",
    })

    client = SoftHouse(
        api_key="sk_test_...",
        base_url="https://test.api.soft.house",
    )

    wish = await client.wishes.create(
        WishCreateParams(
            query="Test wish",
            budget=WishBudget(max=100),
        )
    )

    assert wish.id == "wish_test_1"
    assert wish.status.value == "active"

Complete Example

End-to-end wish fulfillment flow:

import asyncio
import uuid
from soft_house import (
    SoftHouse,
    WishCreateParams,
    WishBudget,
    MandateCreateParams,
    MandateType,
    ProtocolType,
    MerchantInfo,
    PaymentCreateParams,
    SoftHouseError,
)

async def fulfill_wish():
    client = SoftHouse(api_key="sk_test_...")

    try:
        # 1. Create a wish
        wish = await client.wishes.create(
            WishCreateParams(
                query="Find me a mechanical keyboard under $200",
                budget=WishBudget(max=200, currency="USD"),
            )
        )
        print(f"1. Wish created: {wish.id}")

        # 2. Create an AP2 mandate to authorize payment
        mandate = await client.mandates.create(
            MandateCreateParams(
                type=MandateType.INTENT,
                protocol_type=ProtocolType.AP2,
                max_amount=200.00,
                currency="USD",
                merchant_info=MerchantInfo(
                    name="KeyboardCo",
                    domain="keyboards.example.com",
                ),
            )
        )
        print(f"2. Mandate created: {mandate.id}")

        # 3. Verify mandate signature
        verify = await client.mandates.verify(mandate.id)
        print(f"3. Mandate valid: {verify.valid}")

        # 4. Process payment (server recalculates amount)
        payment = await client.payments.create(
            PaymentCreateParams(
                mandate_id=mandate.id,
                amount=149.99,
                currency="USD",
                idempotency_key=str(uuid.uuid4()),
                metadata={"wish_id": wish.id},
            )
        )
        print(f"4. Payment: {payment.id} โ€” {payment.status}")

    except SoftHouseError as e:
        print(f"Error: {e} (status: {e.status_code})")

asyncio.run(fulfill_wish())

Package Structure

soft_house/
  __init__.py     # SoftHouse client + public API exports
  client.py       # SoftHouseHttpClient (HTTP + retry logic)
  errors.py       # Error hierarchy (7 error classes)
  models.py       # Pydantic v2 models (frozen, typed)
  wishes.py       # WishesApi (create, get, update, list, cancel)
  mandates.py     # MandatesApi (create, get, revoke, verify, list)
  payments.py     # PaymentsApi (create, get, list)
  py.typed        # PEP 561 marker for mypy support

Status: the soft-house package is not yet on PyPI and has no public repository โ€” the source lives in the monorepo. Use the REST API directly until the PyPI publish lands.