> ## 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.

# SDK Python

> Pakiet itemshop dla Pythona: Storefront, klient z kluczem, async, błędy i weryfikacja webhooków.

Oficjalne SDK Pythona (≥ 3.10): wersja synchroniczna **i** asynchroniczna (httpx), modele pydantic v2, `mypy --strict`. Odpowiada `@itemshop/sdk` z TypeScript.

| | Uwierzytelnienie | Gdzie używać |
| - | - | - |
| `Storefront` / `AsyncStorefront` | brak (publiczne Storefront API v1) | boty Discord, skrypty serwera gry, buildy stron |
| `Client` / `AsyncClient` | klucz API (`X-API-Key`) | tylko Twój backend — **nigdy** w przeglądarce ani u gracza |

## Instalacja

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

## Storefront (publiczne, bez sekretów)

```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
```

### Kasa

Strona nigdy nie dotyka danych płatniczych: `checkout.create` zwraca `redirect_url` (bramka albo strona statusu dla darmowego zamówienia). SDK dodaje losowy `Idempotency-Key` i ponawia błędy sieci **z tym samym kluczem**, więc zerwane połączenie nie utworzy dwóch zamówień. Koszyk ma 1–20 pozycji; dwie lub więcej tworzą jedną grupę płatności (`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")],   # do 20 pozycji
    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=<numer>&itemshop_key=<klucz>
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)      # klucze, tekst, link do pliku
# result.order_number i result.status_key działają tak samo zaraz po kasie
```

Własny klucz (`idempotency_key=...`) zapisz razem z koszykiem, jeśli ponowienia mają być idempotentne po restarcie procesu; `itemshop.new_idempotency_key()` generuje poprawny.

### Vouchery i widżety

```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", ...]
```

Widżet wyłączony przez właściciela sklepu zwraca puste dane.

## Klient (klucz API, po stronie serwera)

Klucz utworzysz w panelu: **Ustawienia → Klucze API** — nadaj tylko potrzebne uprawnienia. Zobacz [API z kluczem](https://andcze.mintlify.site/pl/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)
```

Zasoby: `shops`, `team`, `servers`, `products`, `payment_methods`, `blacklist`, `discounts`, `vouchers`, `stats`, `uploads`, `orders`, `payment_errors`, `site` (`files`, `draft`, `revisions`, `builds`, `deployments`), `templates`, `agents`, `skills`. Ustawienia i tokeny AI Workspace są tylko dla panelu (klucze API są tam odrzucane), więc nie ma ich w kliencie. Wersja async jest identyczna z `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)
```

### Strona: wdrożenie z 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
```

## Błędy

Każdy wyjątek to `itemshop.ItemShopError` z zawsze ustawionym `code` (UPPER\_SNAKE\_CASE): kod z backendu, jeśli był (`CART_TOO_LARGE`, `LEGAL_DOCUMENTS_CHANGED`…), inaczej wyprowadzony ze statusu (`NOT_FOUND`, `RATE_LIMITED`…), inaczej kod SDK (`NETWORK_ERROR`, `TIMEOUT`, `CONFIG_ERROR`, `VALIDATION_FAILED`). `status` wynosi 0, gdy nie było odpowiedzi. Odpowiedzi spoza 2xx rzucają podklasę `itemshop.APIStatusError`:

```python theme={null}
import itemshop

try:
    sf.checkout.create(...)
except itemshop.BadRequestError as e:
    if e.code == itemshop.ErrorCode.LEGAL_DOCUMENTS_CHANGED:
        ...                                   # pobierz sklep ponownie i zapytaj o zgody jeszcze raz
    elif e.code == itemshop.ErrorCode.CAPTCHA_REQUIRED:
        ...                                   # pokaż Turnstile, ponów z captcha_token=
except itemshop.RateLimitError as e:
    time.sleep(e.retry_after or 5)
except itemshop.APIConnectionError:
    ...                                       # brak sieci / timeout (APITimeoutError)
except itemshop.ValidationError as e:         # odrzucone przed wysłaniem, np. e.code == "LEGAL_CONSENT_REQUIRED"
    ...
```

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

## Ponowienia i limity czasu

GET oraz zapisy z `Idempotency-Key` (kasa) są ponawiane po błędach sieci i statusach 408/425/429/500/502/503/504; inne zapisy tylko po 429. Backoff wykładniczy od 300 ms, maks. 5 s, z jitterem, z poszanowaniem `Retry-After`. Zasady są wspólne dla wszystkich SDK ItemShop.

```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
```

## Webhooki

Backend nie wysyła jeszcze podpisanych webhooków wychodzących (zamówienia trafiają na Discord i do pluginu) — zob. [Webhooki](https://andcze.mintlify.site/pl/developers/webhooks.md). `Webhook` implementuje planowany schemat [Standard Webhooks](https://www.standardwebhooks.com) (`webhook-id`, `webhook-timestamp`, `webhook-signature: v1,<base64 HMAC-SHA256>`), więc możesz pisać kod już dziś.

```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)
    ...
```

## Modele

Modele żądań i odpowiedzi są w `itemshop.models` (pydantic v2). Atrybuty to `snake_case`, format sieciowy zostaje `camelCase`. Pola nieznane SDK trafiają do `model.model_extra`, więc nowszy backend nie psuje starszego SDK. `model.to_dict()` zwraca słownik w formacie sieciowym.

> **[Storefront API](https://andcze.mintlify.site/pl/developers/storefront-api.md)**
>
> Endpointy pod spodem SDK.

> **[Błędy i limity](https://andcze.mintlify.site/pl/developers/errors-and-limits.md)**
>
> Kody błędów i limity zapytań.


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