Metadata-Version: 2.4
Name: fazercards
Version: 0.2.0
Summary: Official Python SDK for the FazerCards reseller API — gift cards, game top-ups, game keys, Steam and Telegram products through one REST API.
Author-email: FazerCards <support@fazercards.com>
License: MIT
Project-URL: Homepage, https://reseller.fazercards.com
Project-URL: Documentation, https://reseller.fazercards.com/en/docs
Project-URL: Cookbook, https://reseller.fazercards.com/en/docs/cookbook
Project-URL: Source, https://github.com/FZR-cards/fazercards-python
Project-URL: Issues, https://github.com/FZR-cards/fazercards-python/issues
Project-URL: Changelog, https://github.com/FZR-cards/fazercards-python/releases
Keywords: fazercards,reseller,gift-cards,gift-card-api,game-topup,pubg-uc,free-fire,roblox,steam-wallet,wholesale,b2b,telegram-bot,aiogram,discord-bot,usdt,crypto-payments,binance-pay,webhooks,rest-api
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx<1.0,>=0.25
Requires-Dist: typing_extensions>=4.7; python_version < "3.11"
Dynamic: license-file

# fazercards

[![PyPI](https://img.shields.io/pypi/v/fazercards.svg)](https://pypi.org/project/fazercards/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)

Official Python SDK for the **FazerCards** reseller API — gift cards, direct game
top-ups, game keys, Steam gifts and wallet funding, Telegram Stars/Premium and
operator-fulfilled services through one REST API.

Sync and async clients, typed responses, automatic retries, safe idempotency
defaults and webhook signature verification.

```bash
pip install fazercards
```

> **0.2.0 is a corrective release.** 0.1.x shipped against endpoints that do not
> exist (`/catalog`, `/order`) and pointed at the wrong host, so no order could
> ever succeed. If you are on 0.1.x, see [Migrating from 0.1.x](#migrating-from-01x).

---

## Quick start

```python
import os
from fazercards import FazerCardsClient

fz = FazerCardsClient(api_key=os.environ["FAZER_API_KEY"])

print(fz.balance.get())          # {'balance': '30.9233', 'currency': 'USD'}

# 1. Pick a product
cards = fz.giftcards.cards("roblox_ru")
offer = cards["offers"][0]
print(offer["name"], offer["price_usd"], "stock:", offer["stock"])

# 2. Buy it
order = fz.giftcards.order(
    category_id="roblox_ru",
    card_id=offer["card_id"],
    quantity=1,
)
print(order["id"], order["status"])   # ord-437891 completed

# 3. Read the delivered codes
print(order["cards"])                 # ['RAUYRB5DQAKPNN5PKW']
```

The API key goes in `api_key=` or the `FAZER_API_KEY` environment variable.

### Async

Every method mirrors the sync one:

```python
import asyncio
from fazercards import FazerCardsAsyncClient

async def main():
    async with FazerCardsAsyncClient(api_key="fc_…") as fz:
        offers = await fz.topups.offers("pubg_mobile_auto")
        order = await fz.topups.order(
            category_id="pubg_mobile_auto",
            offer_id=offers["offers"][0]["offer_id"],
            fields={"player_id": "5123456789"},
        )
        final = await fz.orders.wait(order["id"])   # polls until terminal
        print(final["status"])

asyncio.run(main())
```

---

## Product families

There is **no** unified catalog or order endpoint — each family has its own
listing, its own offer list and its own order body.

| Family | Browse | Offers | Order |
| --- | --- | --- | --- |
| Gift cards | `fz.giftcards.categories()` | `fz.giftcards.cards(category_id)` | `fz.giftcards.order(category_id=, card_id=, quantity=)` |
| Game top-ups | `fz.topups.categories()` | `fz.topups.offers(category_id)` | `fz.topups.order(category_id=, offer_id=, fields={})` |
| Game keys | `fz.gamekeys.games()` | `fz.gamekeys.keys(game_id)` | `fz.gamekeys.order(game_id=, key_id=, quantity=)` |
| Steam gifts | `fz.steam_gifts.games()` | `fz.steam_gifts.game(appid)` | `fz.steam_gifts.order(invite_url=, sub_id=, app_id=, region=)` |
| Steam wallet | `fz.steam_topup.rates()` | — | `fz.steam_topup.order(steam_login=, currency=, amount=)` |
| Telegram | `fz.telegram.stars()` / `.premium()` | — | `fz.telegram.buy_stars(...)` / `.buy_premium(...)` |
| Manual services | `fz.manual_services.list()` | `fz.manual_services.offers(id)` | `fz.manual_services.order(manual_service_id=, product_id=)` |

Account and money: `fz.balance`, `fz.me`, `fz.subscription`, `fz.orders`,
`fz.transactions`, `fz.payments`, `fz.webhooks`.

Catalogs are cursor-paginated; the iterator helpers walk every page for you:

```python
for category in fz.giftcards.iter_categories():
    print(category["category_id"], category["name"])
```

### Top-ups need buyer fields

Each top-up category declares what the buyer must enter. Read it, don't guess:

```python
offers = fz.topups.offers("mobile_legends")
print([f["key"] for f in offers["fields"]])   # ['player_id', 'zone_id']

fz.topups.order(
    category_id="mobile_legends",
    offer_id=offers["offers"][0]["offer_id"],
    fields={"player_id": "123456", "zone_id": "2214"},
)
```

Some games can validate the player id **before** you charge anything:

```python
fz.topups.validate_id(category_id="pubg_mobile", fields={"player_id": "5123456789"})
```

> The ids from `validate_id_games()` (`pubg_mobile`, `free_fire`, …) belong to
> the validation namespace and are **not** purchasable. To sell, use an id from
> `topups.categories()` — e.g. `pubg_mobile_auto`.

---

## Orders

```python
order = fz.orders.get("ord-437891")
orders = fz.orders.list(page=1, limit=50)
for o in fz.orders.iter_all():
    ...
```

Statuses are `created`, `processing`, `completed`, `failed`, `refund`.
The last three are terminal; `refund` means the order could not be fulfilled and
the balance was returned.

Gift cards and game keys are usually instant. Top-ups and Steam are not — wait
for them instead of polling by hand:

```python
final = fz.orders.wait("ord-437891", timeout=600, interval=5)
```

`wait()` polls every 5 s by default, which stays well inside the 120 requests/min
order-status budget, and raises `FazerCardsTimeoutError` if the deadline passes
(the order stays live — the webhook will still fire).

---

## Idempotency — on by default

Every order method sends an `Idempotency-Key` unless you opt out. Retrying the
same key returns the original order instead of charging again, for **7 days**,
including after the order reached `failed`.

```python
# Recommended: your own key, stable across your retries.
fz.giftcards.order(
    category_id="roblox_ru", card_id="800_robux", quantity=1,
    idempotency_key=f"shop-order-{internal_id}",
)

fz.giftcards.order(..., idempotency_key="auto")  # default: fresh UUID per call
fz.giftcards.order(..., idempotency_key=None)    # opt out (not recommended)
```

Reusing one key for a different product type returns HTTP 409
(`FazerCardsConflictError`).

The SDK **never** blind-retries a state-changing request that has no idempotency
key: on a timeout or a 5xx it raises instead, because the server may already have
charged the account. HTTP 429 is always retried — the request was rejected before
any work happened.

---

## Webhooks

Point FazerCards at your endpoint and read the signing secret:

```python
fz.webhooks.set(url="https://shop.example.com/webhooks/fazercards")
secret = fz.webhooks.get()["secret"]
fz.webhooks.test()          # {'ok': True, 'status': 200}
```

Deliveries arrive as `POST` with:

| Header | Value |
| --- | --- |
| `X-Webhook-Signature` | `sha256=<hex>` — HMAC-SHA256 of the **raw body** |
| `X-Webhook-Event` | `order.created`, `order.status_changed`, `manual_service.chat.message`, `manual_service.chat.waiting_reply`, `webhook.test` |
| `X-Webhook-Event-Id` | UUID v4, stable across retries — deduplicate on it |
| `X-Webhook-Delivery-Attempt` | 1-based attempt number |

Body: `{"event": …, "event_id": …, "timestamp": …, "data": {…}}`.

```python
from fastapi import FastAPI, Request, HTTPException
from fazercards import parse_webhook_event, WebhookSignatureError

app = FastAPI()

@app.post("/webhooks/fazercards")
async def hook(request: Request):
    raw = await request.body()                       # RAW bytes, not the parsed JSON
    sig = request.headers.get("X-Webhook-Signature", "")
    try:
        event = parse_webhook_event(raw, sig, os.environ["FZ_WEBHOOK_SECRET"])
    except WebhookSignatureError:
        raise HTTPException(401, "bad signature")

    if event["event"] == "order.status_changed":
        handle(event["data"])
    return {"ok": True}
```

Retries: up to 3 (after ~1 min, ~5 min, ~30 min) on any non-2xx or a 10-second
timeout. After 50 consecutive failures the endpoint is auto-disabled — re-enable
it with `fz.webhooks.set(...)` once the receiver is fixed.

---

## Rate limits

Counters are per category **and** per API key, so polling never eats the
order-creation budget.

| Category | Routes | Limit |
| --- | --- | --- |
| Catalog read | `/giftcards`, `/giftcards/cards`, `/topups`, `/topups/offers`, `/gamekeys`, … | 300/min |
| Create order | every `POST …/order` | 60/min |
| Order status | `GET /orders/{id}` | 120/min |
| Account read | `/me`, `/balance`, `/subscription`, `/transactions` | 60/min |
| Payment write | `POST /payments/create` | 15/min |

On HTTP 429 the SDK honours `Retry-After` and retries with jitter; after the
retry budget it raises `FazerCardsRateLimitError`.

---

## Errors

```python
from fazercards import (
    FazerCardsError,            # base — catches everything below
    FazerCardsAuthError,        # 401 / 403
    FazerCardsBadRequestError,  # 400
    FazerCardsNotFoundError,    # 404
    FazerCardsConflictError,    # 409 (idempotency key reused across products)
    FazerCardsRateLimitError,   # 429 — .retry_after_seconds
    FazerCardsServerError,      # 5xx
    FazerCardsTimeoutError,     # orders.wait() deadline — .last_status, .order
)
```

Each carries `.status`, `.code` and `.response_body`.

---

## Configuration

```python
FazerCardsClient(
    api_key="fc_…",                          # or FAZER_API_KEY
    base_url="https://api.fzr.cards/api/v2", # default
    timeout=30.0,
    retries=3,
    app_name="my-shop/1.2",                  # prepended to the User-Agent
    http_client=my_httpx_client,             # bring your own pool
)
```

---

## Migrating from 0.1.x

0.1.x called endpoints that never existed. Everything below is a rename, not a
behaviour change.

| 0.1.x | 0.2.0 |
| --- | --- |
| `fz.catalog.list()` | `fz.giftcards.categories()` / `fz.topups.categories()` / `fz.gamekeys.games()` |
| `fz.orders.create(sku_id=…)` | `fz.giftcards.order(...)`, `fz.topups.order(...)`, `fz.gamekeys.order(...)`, … |
| `fz.orders.get(id)` → `/order/{id}` | `fz.orders.get(id)` → `/orders/{id}` |
| `fz.payments.create(amount_usd=…)` | `fz.payments.create(amount=…)` |
| `balance["balance_usd"]` | `balance["balance"]` |
| `order["code"]` / `order["codes"]` | `order["cards"]` (gift cards) / `order["keys"]` (game keys) |
| status `pending` / `refunded` | `created` / `processing` / `refund` |
| default host `api.fazercards.com` | `api.fzr.cards` |
| `verify_webhook_signature` vs bare hex | accepts the real `sha256=<hex>` header |

`fz.catalog.*` and the old order helper now raise `FazerCardsError` with a
pointer to the right method instead of failing with a 404.

---

## Links

- API reference: <https://reseller.fazercards.com/en/docs>
- Interactive docs: <https://api.fzr.cards/public/docs>
- Webhooks: <https://reseller.fazercards.com/en/docs/webhooks>
- Cookbook: <https://reseller.fazercards.com/en/docs/cookbook>
- Node / TypeScript SDK: <https://github.com/FZR-cards/fazercards-node>

MIT © FazerCards
