> ## Documentation Index
> Fetch the complete documentation index at: https://andcze.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK

> The itemshop Python package: Storefront, the API-key client, async, errors and webhook verification.

The official Python SDK (≥ 3.10): sync **and** async (httpx), pydantic v2 models, `mypy --strict` clean. Mirrors `@itemshop/sdk` in TypeScript.

| | Auth | Use it in |
| - | - | - |
| `Storefront` / `AsyncStorefront` | none (public Storefront API v1) | Discord bots, game-server scripts, site builds |
| `Client` / `AsyncClient` | API key (`X-API-Key`) | your backend only — **never** a browser or a player |

## Installation

```bash theme={null}
pip install itemshop        # or: uv add itemshop
```

## Storefront (public, no secrets)

```python theme={null}
from itemshop import Storefront

sf = Storefront("664f1a2b3c4d5e6f7a8b9c0d", locale="pl")   # id, uniqueName or domain; or $ITEMSHOP_SHOP_ID
# sf = Storefront.from_domain("sklep.mojserwer.pl")

shop = sf.shop.get()
print(shop.display_name, shop.currency, [s.name for s in shop.servers if s.is_online])

catalog = sf.products.list()                       # .categories (= the shop's servers) + .products
for p in sf.products.list(category=catalog.categories[0].id).products:
    print(f"{p.name}: {p.price} {p.currency}")

vip = sf.products.get("vip")                       # slug or id
methods = sf.payment_methods.list()                # one entry per channel: code, channel, limits
quote = sf.discounts.validate("LATO10", [{"productId": vip.id}])   # quote.valid / quote.reason
```

### Checkout

The site never touches payment data: `checkout.create` returns a `redirect_url` (the gateway, or the shop's status page for a free order). The SDK adds a random `Idempotency-Key` and retries network failures **with the same key**, so a flaky connection never creates two orders. A cart has 1–20 lines; two or more form one payment group (`result.group_id`, `result.order_ids`).

```python theme={null}
from itemshop import CartItem, CheckoutConsents

result = sf.checkout.create(
    items=[CartItem(product_id=vip.id, quantity=1, variant="30 dni")],   # up to 20 lines
    nick="Steve",
    email="steve@example.com",
    consents=CheckoutConsents(
        terms=True,
        immediate_delivery=True,
        legal_version=shop.legal.version,   # the legal documents the buyer was shown
    ),
    payment_method_code=methods[0].code,
    payment_channel=methods[0].channel,
    return_url="https://sklep.mojserwer.pl/dziekujemy",
    captcha_token=token_from_widget,        # only when the shop asks for a captcha
)
redirect(result.redirect_url)               # result.free → nothing to pay

# On the return page:
returned = itemshop.returned_order_from_return_url(request.url)   # ?itemshop_order=<number>&itemshop_key=<key>
if returned:
    status = sf.orders.status_by_number(returned.number, returned.key)   # pending → paid → completed
    if status.delivery and status.delivery.ready:
        delivery = sf.orders.delivery(returned.number, returned.key)      # keys, text, download link per line
# result.order_number and result.status_key work the same way right after checkout
```

Store your own key (`idempotency_key=...`) with the cart if retries must stay idempotent across process restarts; `itemshop.new_idempotency_key()` generates a valid one.

### Vouchers and widgets

```python theme={null}
sf.vouchers.redeem("ABCD-1234", "Steve", turnstile_token=token)   # → order_number, status_key, product_name

sf.widgets.recent_purchases(limit=5)    # [nick, product_name, server_name, amount, created_at]
sf.widgets.top_spenders(limit=10)       # [nick, total_spent, order_count]
sf.widgets.monthly_goal()               # enabled, percent
sf.widgets.online_players()             # count, players: ["Steve", ...]
```

A widget the shop owner switched off returns empty data.

## Client (API key, server-side)

Create a key in the panel under **Settings → API keys** with only the permissions you need. See [API with a key](https://andcze.mintlify.site/en/developers/api-keys.md).

```python theme={null}
from itemshop import Client

client = Client(api_key="isk_...")          # or $ITEMSHOP_API_KEY
shop_id = "664f1a2b3c4d5e6f7a8b9c0d"

# every page, lazily
for order in client.orders.list(shop_id, status="completed", from_date="2026-10-01"):
    print(order.order_number, order.customer.nickname if order.customer else "?")

page = client.orders.list(shop_id, limit=50)   # just the first page
page.items, page.total, page.pages, page.has_next
next_page = page.next_page()

product = client.products.create(shop_id, name="VIP", price=19.99, server_id="...",
                                 commands=["lp user {PLAYER} parent set vip"])
client.products.update(shop_id, product.id, price=14.99)
client.orders.fulfill(shop_id, order_id)
client.discounts.create(shop_id, code="LATO10", scope="all", type="percentage", value=10, max_usage=100)
```

Resources: `shops`, `team`, `servers`, `products`, `payment_methods`, `blacklist`, `discounts`, `vouchers`, `stats`, `uploads`, `orders`, `payment_errors`, `site` (`files`, `draft`, `revisions`, `builds`, `deployments`), `templates`, `agents`, `skills`. AI Workspace settings and tokens are panel-only (API keys are rejected there), so they are not part of this client. Async is identical with `await` / `async for`:

```python theme={null}
from itemshop import AsyncClient

async with AsyncClient() as client:
    async for order in client.orders.list(shop_id):
        ...
    first_page = await client.orders.list(shop_id)
```

### Site: deploy from CI

```python theme={null}
client.site.publish(shop_id, "./dist", message="CI build")       # upload → build → deploy production
client.site.upload(shop_id, "site.zip")                          # just a new revision

client.site.files.list(shop_id)                                  # draft; rev="<id>" for a revision
client.site.files.write(shop_id, "src/site.config.ts", source)
client.site.files.read_text(shop_id, "src/App.tsx")
client.site.draft.commit(shop_id, message="New colors")          # → SiteRevision
build = client.site.builds.create(shop_id, revision_id="draft")
build = client.site.builds.wait(shop_id, build.id)
print(client.site.builds.log(shop_id, build.id))
dep = client.site.deployments.create(shop_id, build_id=build.id, kind="preview")
client.site.deployments.promote(shop_id, older_deployment_id)    # rollback
```

## Errors

Every exception is an `itemshop.ItemShopError` with an always-set `code` (UPPER\_SNAKE\_CASE): the backend's code when it sent one (`CART_TOO_LARGE`, `LEGAL_DOCUMENTS_CHANGED`…), otherwise one derived from the status (`NOT_FOUND`, `RATE_LIMITED`…), otherwise an SDK code (`NETWORK_ERROR`, `TIMEOUT`, `CONFIG_ERROR`, `VALIDATION_FAILED`). `status` is 0 when no response arrived. Non-2xx responses raise a subclass of `itemshop.APIStatusError`:

```python theme={null}
import itemshop

try:
    sf.checkout.create(...)
except itemshop.BadRequestError as e:
    if e.code == itemshop.ErrorCode.LEGAL_DOCUMENTS_CHANGED:
        ...                                   # re-fetch shop.legal and ask again
    elif e.code == itemshop.ErrorCode.CAPTCHA_REQUIRED:
        ...                                   # show Turnstile, retry with captcha_token=
except itemshop.RateLimitError as e:
    time.sleep(e.retry_after or 5)
except itemshop.APIConnectionError:
    ...                                       # network down / timeout (APITimeoutError)
except itemshop.ValidationError as e:         # rejected before sending, e.g. e.code == "LEGAL_CONSENT_REQUIRED"
    ...
```

| Exception | Status |
| - | - |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401 |
| `PermissionDeniedError` | 403 (key lacks a permission) |
| `NotFoundError` | 404 |
| `ConflictError` | 409 (`IDEMPOTENCY_KEY_REUSED`, `CHECKOUT_IN_PROGRESS`…) |
| `UnprocessableEntityError` | 422 |
| `RateLimitError` | 429 (`.retry_after`) |
| `InternalServerError` | 5xx |

## Retries and timeouts

GET requests and writes carrying an `Idempotency-Key` (checkout) are retried on network errors and 408/425/429/500/502/503/504; any other write only on 429. Backoff is exponential from 300 ms, capped at 5 s, with jitter, honouring `Retry-After`. The rules are shared by all ItemShop SDKs.

```python theme={null}
Client(api_key=..., max_retries=4, timeout=10.0)
Client(api_key=..., http_client=httpx.Client(proxy="http://proxy:3128"))   # bring your own httpx client
Client(api_key=..., base_url="http://localhost:3000")                      # local backend
```

## Webhooks

The backend does not send signed outgoing webhooks yet (orders reach Discord and the game plugin directly) — see [Webhooks](https://andcze.mintlify.site/en/developers/webhooks.md). `Webhook` implements the planned [Standard Webhooks](https://www.standardwebhooks.com) scheme (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256>`), so you can write the receiver today.

```python theme={null}
from itemshop import Webhook, WebhookVerificationError

wh = Webhook(os.environ["ITEMSHOP_WEBHOOK_SECRET"])          # "whsec_..."

@app.post("/webhooks/itemshop")                               # Flask
def itemshop_webhook():
    try:
        event = wh.verify(request.get_data(), request.headers)   # raw body!
    except WebhookVerificationError:
        abort(400)
    ...
```

## Models

Request/response models live in `itemshop.models` (pydantic v2). Attributes are `snake_case`; the wire format stays `camelCase`. Fields unknown to the SDK are kept in `model.model_extra`, so a newer backend never breaks an older SDK. `model.to_dict()` returns the wire-format dict.

> **[Storefront API](https://andcze.mintlify.site/en/developers/storefront-api.md)**
>
> The endpoints under the SDK.

> **[Errors and limits](https://andcze.mintlify.site/en/developers/errors-and-limits.md)**
>
> Error codes and rate limits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.