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.
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.
Install
pip install "chipi-stack>=2.1.0" httpx
Initialise the SDK
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.
# 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").
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:
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
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:
# 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
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. React-side rendering? See the React guide.