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
)
| Parameter | Type | Default | Description |
|---|---|---|---|
api_key | str | None | None | API key (sk_live_... or sk_test_...) |
base_url | str | https://api.soft.house | API base URL |
timeout | float | 30.0 | Request timeout (seconds) |
max_retries | int | 3 | Max 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
| Error | HTTP Status | Retryable | Description |
|---|---|---|---|
AuthenticationError | 401 | No | Invalid or missing API key |
ForbiddenError | 403 | No | Insufficient permissions |
NotFoundError | 404 | No | Resource does not exist |
ValidationError | 400 | No | Invalid request parameters |
ConflictError | 409 | No | Resource conflict |
RateLimitError | 429 | Yes | Rate limit exceeded |
NetworkError | - | Yes | Network-level failure |
SoftHouseError | * | Depends | Base class for all errors |
Retry Logic
The SDK automatically retries on:
- 5xx server errors โ exponential backoff with jitter
- 429 rate limit โ respects
Retry-Afterheader - 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-housepackage 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.