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

# Bill Payments — Python

> Browse the SKU catalog, submit a bill payment against your Chipi credits, and poll for settlement using the chipi-stack Python SDK plus httpx. No end-user wallet, no on-chain signing.

The `chipi-stack` Python package exposes the read side of Bills (`get_sku_list`, `get_sku`, `get_sku_purchase`). The purchase itself is a plain HTTP POST against `/v1/sku-purchases` — the SDK's `purchase_sku` method wraps an on-chain wallet path that's reserved for Chipi's internal PWA, not what you want for API-key integrations.

<Note>
  Every endpoint on this page is guarded by Chipi's `BearerTokenGuard` and requires *both* an `x-api-key` header *and* a customer JWT in the `Authorization: Bearer` header. The JWT is issued by *your* auth provider; Chipi validates it against the JWKS URL you register for this API key. The Chipi API secret key (`sk_dev_...` / `sk_prod_...`) is **not** a valid Bearer token — passing it where a customer JWT is expected will 401.
</Note>

## Install

<CodeGroup>
  ```bash pip theme={null}
  pip install "chipi-stack>=2.1.0" httpx
  ```

  ```bash uv theme={null}
  uv add "chipi-stack>=2.1.0" httpx
  ```

  ```bash poetry theme={null}
  poetry add "chipi-stack>=2.1.0" httpx
  ```
</CodeGroup>

## Initialise the SDK

```python theme={null}
import os
from chipi_sdk import ChipiSDK, ChipiSDKConfig

sdk = ChipiSDK(
    config=ChipiSDKConfig(
        api_public_key=os.environ["CHIPI_PUBLIC_KEY"],
        api_secret_key=os.environ["CHIPI_SECRET_KEY"],
    )
)
```

## Obtain a customer JWT

Every call needs a customer JWT for the `Authorization: Bearer` header. In a typical server-side flow you'd have one for the user making the purchase — from your auth provider's session, an exchange of an API token, etc.

```python theme={null}
# however you obtain a signed session token for the requesting user —
# e.g. from Clerk's Python SDK, your custom JWT service, or an exchange
# flow against your auth provider.
bearer_token: str = get_current_user_jwt()
```

## Browse the catalog — `get_sku_list`

`get_sku_list` accepts the same filters as the Node SDK: `category` (`SkuCategory` enum), `chipi_category` (curated taxonomy: `"RECARGAS"`, `"GIFT_CARDS"`, `"GENERAL"`, `"TELEFONIA"`), `carrier_name` (case-insensitive substring), `provider` (`"TET" | "CHIPI"`).

```python theme={null}
from chipi_sdk import GetSkuListQuery

result = sdk.get_sku_list(
    params=GetSkuListQuery(
        chipi_category="RECARGAS",
        carrier_name="Telcel",
        limit=20,
    ),
    bearer_token=bearer_token,
)

print(f"{result.total} matching SKUs")
print(result.data[0].name, result.data[0].fixed_amount, result.data[0].currency)
```

The async variant is `aget_sku_list`. Same applies to all paired methods on this page.

## Submit a purchase — direct `POST /v1/sku-purchases`

The SDK's `purchase_sku` requires an on-chain wallet path that's reserved for Chipi's internal PWA. For API-key integrations, post to the REST endpoint directly. Body matches the `CreateSkuPurchaseInput` DTO:

```python theme={null}
import os
import uuid
import httpx

def buy_sku(
    chosen_sku,
    user_phone: str,
    bearer_token: str,
    *,
    org_markup: float = 0,
) -> dict:
    """Debit org credits + queue carrier fulfillment. Returns the
    freshly-created Transaction row (status="PENDING" initially)."""
    body = {
        # transactionHash is used purely as an idempotency key — any
        # unique string works (UUID, your internal order id, etc.).
        # Re-sending the same value within today returns the existing
        # Transaction row instead of double-charging.
        "transactionHash": f"order-{uuid.uuid4()}",
        "skuId": chosen_sku.id,
        "skuReference": user_phone,
        "currencyAmount": chosen_sku.fixed_amount,
        "chain": "STARKNET",
        "token": "USDC",
        "walletAddress": "0x0",   # required by the DTO but unused for credits flow
        "orgMarkup": org_markup,  # optional analytics field
    }
    response = httpx.post(
        "https://api.chipipay.com/v1/sku-purchases",
        headers={
            "Content-Type": "application/json",
            "x-api-key": os.environ["CHIPI_PUBLIC_KEY"],
            "Authorization": f"Bearer {bearer_token}",
        },
        json=body,
        timeout=15.0,
    )
    response.raise_for_status()
    return response.json()

transaction = buy_sku(chosen_sku, "5512345678", bearer_token)
purchase_id = transaction["id"]
print(f"Purchase created: {purchase_id} (status={transaction['status']})")
```

For async code paths, swap `httpx.post` for `httpx.AsyncClient().post` — same body and headers.

## Poll for settlement — `get_sku_purchase`

```python theme={null}
import time

def wait_for_settlement(sdk, purchase_id: str, bearer_token: str):
    for _ in range(12):
        purchase = sdk.get_sku_purchase(
            purchase_id,
            bearer_token=bearer_token,
        )
        if purchase.status in ("SUCCESS", "FAILED"):
            return purchase
        time.sleep(2.5)
    raise TimeoutError("Settlement timeout — set up a webhook for production")

final = wait_for_settlement(sdk, purchase_id, bearer_token)
print(final.status, "file:", final.sku_file_number)
```

Most transactions settle in under 5 seconds. For production traffic, prefer a webhook over polling — configure one at `/configure/notifications` in the dashboard.

## DEV sandbox

When `CHIPI_PUBLIC_KEY` is a DEV key (`pk_dev_...`), every purchase is sandboxed:

* **No real credits debited.** Your `OrgBalance.availableUsd` is never touched.
* **No carrier call.** TET is never invoked.
* **Deterministic SUCCESS.** Every purchase succeeds within milliseconds.

Sandbox responses are tagged so your code can distinguish them:

```python theme={null}
# DEV response shape (key fields)
{
    "id": "tx-...",
    "status": "SUCCESS",                  # always SUCCESS in DEV
    "ledgerEntryId": "le-dev-<uuid>",     # synthetic, not a real DB row
    "skuFileNumber": "dev-sandbox-<ts>",  # not a real carrier receipt
}
```

You can integrate freely against `pk_dev_...` without worrying about credits or accidentally recharging a real phone. Swap the API key to `pk_prod_...` (and point catalog lookups at the production data) and the same code runs against the real carrier with no other changes.

## Putting it together

```python theme={null}
import os, time, uuid
import httpx
from chipi_sdk import ChipiSDK, ChipiSDKConfig, GetSkuListQuery


def pay_telcel_recharge(sdk: ChipiSDK, user_phone: str, bearer_token: str):
    # 1. Find the right SKU — say, Telcel $200 prepaid
    listing = sdk.get_sku_list(
        params=GetSkuListQuery(
            chipi_category="RECARGAS",
            carrier_name="Telcel",
            limit=50,
        ),
        bearer_token=bearer_token,
    )
    sku = next((s for s in listing.data if s.fixed_amount == 200), None)
    if sku is None:
        raise ValueError("SKU not found")

    # 2. Debit credits + queue fulfillment
    response = httpx.post(
        "https://api.chipipay.com/v1/sku-purchases",
        headers={
            "Content-Type": "application/json",
            "x-api-key": os.environ["CHIPI_PUBLIC_KEY"],
            "Authorization": f"Bearer {bearer_token}",
        },
        json={
            "transactionHash": f"order-{uuid.uuid4()}",
            "skuId": sku.id,
            "skuReference": user_phone,
            "currencyAmount": 200,
            "chain": "STARKNET",
            "token": "USDC",
            "walletAddress": "0x0",
        },
        timeout=15.0,
    )
    response.raise_for_status()
    purchase_id = response.json()["id"]

    # 3. Wait for settlement
    for _ in range(12):
        p = sdk.get_sku_purchase(purchase_id, bearer_token=bearer_token)
        if p.status == "SUCCESS":
            return p
        if p.status == "FAILED":
            raise RuntimeError("Purchase failed")
        time.sleep(2.5)

    raise TimeoutError("Settlement timeout — set up a webhook for production")
```

> ✅ Verified against the live API on **2026-05-19** — exact pattern used in the production smoke purchase for `sku-VIR020` (Virgin \$20 MXN), settled SUCCESS in \< 5s.

## What's next

* Browse the catalog visually in the dashboard at `/admin/bills`.
* Configure your per-transaction markup at `/configure/skus`.
* Set up a webhook at `/configure/notifications` so you don't have to poll.
* Need server-side billing in TypeScript? See the [Node guide](/services/bills/node). React-side rendering? See the [React guide](/services/bills/react).
