# CardHook API

**Versiya:** v1
**Oxirgi yangilanish:** 2026-09-22

CardHook — Humo va Uzcard kartalariga kelgan o'tkazmalarni avtomatik aniqlab,
ularni buyurtmalaringizga bog'laydigan servis. Bu hujjat integratsiya uchun
kerak bo'ladigan hamma narsani o'z ichiga oladi.

---

## 1. Muammo va yechim

Karta o'tkazmasi hech qanday buyurtma raqamini olib kelmaydi. Bankdan faqat
shunday xabar keladi:

```
HUMO *0268 Popolnenie 24 017.00 UZS Balans: 1 250 000.00 UZS
```

Bu yerda buyurtma raqami yo'q. Kim to'laganini, qaysi buyurtma uchun
to'laganini aniqlashning yagona ishonchli yo'li — **noyob summa**.

```
1. Platforma 24 000 so'mlik hisob so'raydi
2. CardHook 24 017 so'm qaytaradi          <- oxirgi 2 raqam noyob
3. Mijoz aynan 24 017 o'tkazadi
4. Bank xabari keladi
5. Shu summani kutayotgan yagona hisob topiladi
6. Platformaga imzolangan callback ketadi
```

Qo'shimcha summa 0 dan 99 so'mgacha — mijoz uchun sezilmaydi, lekin ayni
paytda kutilayotgan hisoblar orasida takrorlanmaydi.

### Noyoblik qanday kafolatlanadi

Ikki qatlam:

1. **Kod qatlami** — hisob yaratilganda band summalar o'qiladi va bo'shi
   tanlanadi.
2. **Baza qatlami** — `UNIQUE(pay_amount) WHERE status='pending'` qisman
   indeksi. Ikkita so'rov bir vaqtda kelsa ham baza ikkinchisini rad etadi
   va kod keyingi bo'sh summaga o'tadi.

Shu summa uchun barcha 100 variant band bo'lsa, `no_free_amount` xatosi
qaytadi. Bu vaqtinchalik holat: eski hisoblar muddati tugagach summalar
bo'shaydi.

### Noaniqlikda taxmin yo'q

Agar bitta summaga ikkita hisob to'g'ri kelsa, CardHook **hech birini**
avtomatik tasdiqlamaydi — tushum adminga qo'lda hal qilish uchun boradi.
Bu tizimning eng muhim qoidasi: noto'g'ri buyurtmani yopgandan ko'ra,
qo'lda tekshirgan afzal.

---

## 2. Boshlash

### 2.1. Nima kerak

| | |
|---|---|
| `API_KEY` | so'rovlarni imzolash uchun (`pbx_` bilan boshlanadi) |
| `CALLBACK_SECRET` | callback imzosini tekshirish uchun |
| `callback_url` | to'lov tasdiqlanganda CardHook murojaat qiladigan manzil |

Ikkalasini ham biznes panelidan olasiz: **[panel.cardhook.uz](https://panel.cardhook.uz)
→ API kalitlari**.

| | Qayta ko'rish mumkinmi | Nega |
|---|---|---|
| `API_KEY` | **yo'q** | serverda faqat `sha256` hash saqlanadi. Yo'qotsangiz «yangi kalit» tugmasi bilan yangisini olasiz — eskisi shu zahoti ishlamay qoladi. |
| `CALLBACK_SECRET` | **ha** | imzo hisoblashda kerak, shuning uchun ochiq saqlanadi. «Ko'rsatish» tugmasi bilan istalgan vaqtda olasiz. |

Maxfiy so'z oshkor bo'lsa — «yangi maxfiy so'z» tugmasi. Diqqat: yangilagan
zahoti eski maxfiy so'z bilan imzolangan, hali yetkazilmagan callbacklar
ham tekshiruvdan o'tmay qoladi.

### 2.2. Base URL

| Manzil | Qachon ishlatiladi |
|---|---|
| `http://127.0.0.1:8080` | platforma CardHook bilan **bir serverda** bo'lsa |
| `https://api.cardhook.uz` | platforma **boshqa serverda** bo'lsa |

`api.cardhook.uz` istalgan manzildan ochiladi — IP ro'yxati yo'q.
Autentifikatsiya butunlay `API_KEY` bo'yicha: kalitsiz yoki noto'g'ri
kalit bilan `401` qaytadi. Shuning uchun kalitni sir saqlash MUHIM:
u oshkor bo'lsa, istalgan joydan ishlatilishi mumkin. Oshkor bo'lsa
panelda darhol yangilang.

Bir serverdagi platformalar uchun `127.0.0.1` afzal: tezroq, TLS va DNS
ortiqcha yuki yo'q.

### 2.3. Autentifikatsiya

Har bir `/v1/*` so'rovda:

```
Authorization: Bearer pbx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Kalit **sarlavhada** uzatiladi, so'rov manzilida emas — aks holda u
nginx loglariga, proxy loglariga va brauzer tarixiga tushadi.

Kalit noto'g'ri, yo'q yoki platforma bloklangan bo'lsa — `401 unauthorized`.

### 2.4. Pul birligi

API'da summalar **so'mda**, butun son:

```json
{ "amount": 24000, "pay_amount": 24017 }
```

Ichkarida hammasi tiyinda (`int`) saqlanadi, float umuman ishlatilmaydi —
yaxlitlash xatosi to'lovda qabul qilinmaydi.

Kasr summalar qo'llab-quvvatlanmaydi. `24000.50` yuborsangiz
`invalid_amount` qaytadi.

### 2.5. Vaqt formati

Barcha vaqtlar **UTC**, ISO 8601:

```
2026-09-21T10:30:00Z
```

Server vaqt zonasi (O'zbekistonda UTC+5) API javoblariga ta'sir qilmaydi.
Foydalanuvchiga ko'rsatishda 5 soat qo'shing.

---

## 3. Hisob yaratish

```http
POST /v1/invoices
Authorization: Bearer pbx_xxxxxxxx
Content-Type: application/json
```

### So'rov

```json
{
  "external_id": "order-123",
  "amount": 24000,
  "callback_url": "https://star.uz/cardhook/callback",
  "expires_in": 1800,
  "description": "100 Stars"
}
```

| Maydon | Tur | Majburiy | Izoh |
|---|---|---|---|
| `external_id` | string | **ha** | sizning buyurtma raqamingiz, 1–120 belgi |
| `amount` | integer | **ha** | so'mda, 1 000 – 100 000 000 |
| `callback_url` | string | yo'q | `http://` yoki `https://`. Berilmasa callback yuborilmaydi |
| `expires_in` | integer | yo'q | soniya, 60 – 86400. Default: 1800 (30 daqiqa) |
| `description` | string | yo'q | 255 belgigacha, faqat sizning panelingizda ko'rinadi |

### Javob — 201 Created

```json
{
  "invoice_id": "inv_9f2a4c1b8e7d3a56",
  "external_id": "order-123",
  "amount": 24000,
  "pay_amount": 24017,
  "card": {
    "number": "9860 1701 1203 0268",
    "holder": "ISM FAMILIYA",
    "bank": "HUMO"
  },
  "status": "pending",
  "description": "100 Stars",
  "expires_at": "2026-09-21T10:30:00Z",
  "paid_at": null,
  "created_at": "2026-09-21T10:00:00Z"
}
```

| Maydon | Izoh |
|---|---|
| `invoice_id` | CardHook bergan identifikator, `inv_` + 16 hex belgi |
| `amount` | siz so'ragan summa |
| `pay_amount` | **mijozga ko'rsatiladigan summa** — aynan shu to'lanishi kerak |
| `card` | mijoz pul o'tkazadigan karta |
| `status` | `pending` · `paid` · `expired` · `cancelled` |
| `expires_at` | shundan keyin hisob `expired` bo'ladi va summa bo'shaydi |
| `paid_at` | to'langan vaqt, `pending` holatda `null` |

### Idempotentlik

Bir xil `external_id` bilan qayta so'ralsa **yangi hisob yaratilmaydi** —
mavjudi `200 OK` bilan qaytadi (`201` emas).

Bu tarmoq uzilishidan himoya qiladi: javob yo'lda yo'qolsa, so'rovni
xavfsiz takrorlaysiz va o'sha hisobni olasiz.

```
1-so'rov  external_id=order-123  ->  201  pay_amount 24017
2-so'rov  external_id=order-123  ->  200  pay_amount 24017   (o'sha hisob)
```

Diqqat: takroriy so'rovda `amount` boshqacha bo'lsa ham **e'tiborga
olinmaydi** — mavjud hisob o'z summasi bilan qaytadi. Boshqa summa kerak
bo'lsa boshqa `external_id` ishlating.

`external_id` faqat sizning platformangiz ichida noyob bo'lishi kerak.
Turli platformalar bir xil `external_id` ishlatishi mumkin.

### Xatolar

| HTTP | Kod | Sabab |
|---|---|---|
| 400 | `invalid_amount` | butun son emas, chegaradan tashqarida yoki `bool` |
| 400 | `invalid_request` | `external_id` bo'sh, JSON buzuq, `expires_in` noto'g'ri, `callback_url` http(s) emas |
| 401 | `unauthorized` | kalit yo'q / noto'g'ri / platforma bloklangan |
| 409 | `no_free_amount` | shu summa uchun 100 ta variant ham band |
| 429 | `rate_limited` | daqiqalik limitdan oshdi |
| 503 | `no_active_card` | faol karta yo'q yoki kunlik limit to'lgan |

---

## 4. Hisobni olish

### 4.1. ID bo'yicha

```http
GET /v1/invoices/inv_9f2a4c1b8e7d3a56
Authorization: Bearer pbx_xxxxxxxx
```

Javob — 3-bo'limdagi bilan bir xil obyekt.

### 4.2. O'z raqamingiz bo'yicha

```http
GET /v1/invoices?external_id=order-123
Authorization: Bearer pbx_xxxxxxxx
```

`invoice_id` ni saqlamagan bo'lsangiz qulay.

### Xatolar

| HTTP | Kod | Sabab |
|---|---|---|
| 400 | `invalid_request` | `external_id` parametri berilmagan |
| 401 | `unauthorized` | kalit muammosi |
| 404 | `not_found` | hisob yo'q **yoki boshqa platformaniki** |

Boshqa platformaning hisobi ham `404` qaytaradi — `403` emas. Ya'ni
mavjudligi ham oshkor qilinmaydi.

---

## 5. Hisobni bekor qilish

```http
POST /v1/invoices/inv_9f2a4c1b8e7d3a56/cancel
Authorization: Bearer pbx_xxxxxxxx
```

Faqat `pending` holatdagi hisob bekor qilinadi. Bekor qilingach uning
`pay_amount` summasi darhol bo'shaydi va boshqa hisobga berilishi mumkin.

Javob — hisob obyekti, `status: "cancelled"`.

### Xatolar

| HTTP | Kod | Sabab |
|---|---|---|
| 404 | `not_found` | hisob yo'q yoki boshqa platformaniki |
| 409 | `already_paid` | hisob allaqachon to'langan |

`expired` yoki `cancelled` hisobni bekor qilish xato emas — hisob o'z
holatida qaytadi.

---

## 6. Callback

To'lov aniqlangach CardHook sizning `callback_url` manzilingizga `POST`
yuboradi.

**Callback qaysi manzildan keladi:**

| | |
|---|---|
| Manba IP | `138.249.7.47` |
| `User-Agent` | `PayBox/1.0` |
| Metod | `POST`, `Content-Type: application/json` |

Xohlasangiz o'z tarafingizda shu IP dan boshqasini rad etishingiz mumkin
— lekin bu **imzo tekshirish o'rnini bosmaydi**. IP ni soxtalashtirish
mumkin, imzoni esa yo'q. Imzo har doim tekshirilsin, IP filtri faqat
qo'shimcha qatlam.

Agar proxy (Cloudflare, nginx) orqali qabul qilsangiz, haqiqiy IP
`X-Forwarded-For` sarlavhasida bo'ladi.

```http
POST /cardhook/callback
X-Signature: sha256=9b1f2c...
X-Timestamp: 1790000000
Content-Type: application/json
User-Agent: PayBox/1.0

{"invoice_id":"inv_9f2a4c1b8e7d3a56","external_id":"order-123","amount":24000,"paid_amount":24017,"paid_at":"2026-09-21T10:12:03Z","status":"paid"}
```

| Maydon | Izoh |
|---|---|
| `invoice_id` | CardHook identifikatori |
| `external_id` | sizning buyurtma raqamingiz |
| `amount` | siz so'ragan summa |
| `paid_amount` | mijoz haqiqatan o'tkazgan summa (noyob qo'shimcha bilan) |
| `paid_at` | to'lov tasdiqlangan vaqt, UTC |
| `status` | hozircha faqat `paid` |

### 6.1. Imzoni tekshirish — MAJBURIY

```
HMAC_SHA256(callback_secret, timestamp + "." + raw_body)
```

Imzo **aynan yuborilgan baytlar** ustidan hisoblanadi. JSON'ni qayta
serializatsiya qilmang — bo'sh joy yoki maydon tartibi o'zgarsa imzo
mos kelmaydi.

Imzo tekshirilmasa, istalgan odam sizning manzilingizga "to'landi" deb
POST yuborishi mumkin.

**Python (aiohttp):**

```python
import hashlib
import hmac
import json
import time

SECRET = "sizning_callback_secret"

def verify(raw_body: bytes, timestamp: str, signature: str) -> bool:
    # Eski so'rovlarni rad etamiz (replay himoyasi)
    try:
        if abs(time.time() - int(timestamp)) > 300:
            return False
    except (TypeError, ValueError):
        return False

    expected = "sha256=" + hmac.new(
        SECRET.encode(),
        timestamp.encode() + b"." + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)


async def handle_callback(request):
    raw = await request.read()
    if not verify(
        raw,
        request.headers.get("X-Timestamp", ""),
        request.headers.get("X-Signature", ""),
    ):
        return web.Response(status=403)

    data = json.loads(raw)

    # Idempotentlik - eng muhim qism
    if await already_processed(data["invoice_id"]):
        return web.Response(text="ok")

    await deliver_order(data["external_id"])
    await mark_processed(data["invoice_id"])

    return web.Response(text="ok")   # 2xx bo'lishi SHART
```

**Node.js (Express):**

```javascript
const crypto = require("crypto");
const SECRET = process.env.CARDHOOK_SECRET;

// raw body kerak - express.json() dan OLDIN
app.post(
  "/cardhook/callback",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const ts = req.get("X-Timestamp") || "";
    const sig = req.get("X-Signature") || "";

    if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
      return res.sendStatus(403);
    }

    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", SECRET)
        .update(Buffer.concat([Buffer.from(ts + "."), req.body]))
        .digest("hex");

    const ok =
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.sendStatus(403);

    const data = JSON.parse(req.body.toString());
    if (await alreadyProcessed(data.invoice_id)) return res.send("ok");

    await deliverOrder(data.external_id);
    await markProcessed(data.invoice_id);
    res.send("ok");
  }
);
```

**PHP:**

```php
<?php
$secret = getenv('CARDHOOK_SECRET');
$raw    = file_get_contents('php://input');
$ts     = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$sig    = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

if (abs(time() - (int)$ts) > 300) {
    http_response_code(403);
    exit;
}

$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);

if (!hash_equals($expected, $sig)) {
    http_response_code(403);
    exit;
}

$data = json_decode($raw, true);

if (alreadyProcessed($data['invoice_id'])) {
    echo 'ok';
    exit;
}

deliverOrder($data['external_id']);
markProcessed($data['invoice_id']);
echo 'ok';
```

### 6.2. Qayta urinishlar

Javobingiz `2xx` bo'lmasa (yoki umuman kelmasa), CardHook qayta uradi:

| Urinish | Qachon |
|---|---|
| 1 | darhol |
| 2 | 5 soniyadan keyin |
| 3 | 30 soniyadan keyin |
| 4 | 2 daqiqadan keyin |
| 5 | 10 daqiqadan keyin |
| 6 | 1 soatdan keyin |

Jami 6 urinish, taxminan 1 soat 13 daqiqa. Shundan keyin ham
yetkazilmasa — CardHook admini Telegram orqali ogohlantiriladi va
callback `gave_up` holatiga o'tadi.

So'rov kutish muddati — 15 soniya. Javobingiz shundan sekin bo'lsa
urinish muvaffaqiyatsiz hisoblanadi.

**Tavsiya:** callback'ni tez qaytaring. Og'ir ishni (buyurtma yetkazish,
xabar yuborish) fon vazifasiga qo'ying, `2xx` ni darhol bering.

### 6.3. Idempotentlik — MAJBURIY

Callback **takrorlanishi mumkin**. Masalan javobingiz yo'lda yo'qolsa,
CardHook uni yetkazilmagan deb hisoblab qayta yuboradi.

Shuning uchun qabul qiluvchi tomon `invoice_id` bo'yicha idempotent
bo'lishi **shart**: bitta `invoice_id` uchun buyurtma faqat bir marta
bajarilsin.

Eng oddiy yo'l — bazada `processed_callbacks(invoice_id)` jadvali va
unda `UNIQUE` indeks.

### 6.4. Yetkazilmagan callbackni qayta yuborish

6 urinish tugagach callback to'xtaydi va avtomatik qayta yuborilmaydi.
Agar platformangiz uzoq vaqt o'chiq turgan bo'lsa, biznes panelidagi
**Callbacklar** bo'limiga kiring: har bir yetkazilmagan xabar yonida
**«qayta yuborish»** tugmasi bor.

Bu muhim: hisob CardHook'da `paid` bo'lib turadi, lekin sizning
platformangiz bu haqda bilmaydi — mijoz pul to'lagan, buyurtma esa
bajarilmagan. Qayta yuborish shu holatdan chiqishning yagona yo'li.

Panelda holatlar:

| Holat | Ma'nosi |
|---|---|
| `yetkazildi` | sizning serveringiz 2xx qaytardi |
| `urinilmoqda` | urinishlar davom etmoqda |
| `yetkazilmadi` | 6 urinish tugadi — qo'lda qayta yuborish kerak |

### 6.5. Callback kelmasa nima qilish kerak

Callback — qulaylik, yagona haqiqat manbai emas. Tarmoq muammosi bo'lishi
mumkin. Shuning uchun:

- muhim buyurtmalar uchun `GET /v1/invoices/{id}` bilan davriy tekshiring
  (masalan har 30 soniyada, hisob muddati tugagunicha);
- yoki mijoz "To'ladim" tugmasini bosganda holatni so'rang.

Ikkalasi ham idempotent bo'lgani uchun callback bilan to'qnashmaydi.

---

## 7. Hisob holatlari

```
                  to'lov aniqlandi
   pending ─────────────────────────> paid
      │
      ├── expires_at o'tdi ─────────> expired
      │
      └── /cancel ──────────────────> cancelled
```

| Holat | Ma'nosi |
|---|---|
| `pending` | to'lov kutilmoqda, `pay_amount` band |
| `paid` | to'lov aniqlandi va tasdiqlandi |
| `expired` | muddati o'tdi, summa bo'shadi |
| `cancelled` | siz bekor qildingiz, summa bo'shadi |

`paid` — yakuniy holat. Hisob `pending` dan `paid` ga **faqat bir marta**
o'tadi: bu baza darajasidagi shartli `UPDATE` bilan kafolatlangan, ya'ni
ikkita xabar bir vaqtda kelsa ham bitta to'lov ikki marta hisoblanmaydi.

---

## 8. Cheklovlar

| Cheklov | Qiymat | Oshsa |
|---|---|---|
| So'rovlar | 60 / daqiqa (har platforma uchun) | `429 rate_limited` |
| Summa | 1 000 – 100 000 000 so'm | `400 invalid_amount` |
| Hisob muddati | 60 – 86 400 soniya | `400 invalid_request` |
| Bitta summa uchun faol hisoblar | 100 ta | `409 no_free_amount` |
| So'rov tanasi | 64 KB | `413` |
| `external_id` | 120 belgi | `400 invalid_request` |
| `description` | 255 belgi | kesiladi |

### Qabul qilinmaydigan holatlar

- **Qisman to'lov.** Hisob to'liq to'lanadi yoki umuman to'lanmaydi.
  Bir nechta o'tkazmani bitta hisobga yig'ish qo'llab-quvvatlanmaydi.
- **"Taxminan to'g'ri" summa.** 1 so'm farq ham rad etiladi — aks holda
  noyob summa mexanizmining ma'nosi yo'qoladi.
- **Mijozning karta ma'lumotlari.** CardHook hech qachon PAN yoki CVV
  so'ramaydi va saqlamaydi. U faqat o'z kartasiga kelgan o'tkazmani kutadi.

---

## 9. Xatolar

Barcha xatolar bir xil shaklda:

```json
{
  "error": {
    "code": "no_free_amount",
    "message": "Bu summa uchun bo'sh noyob qiymat qolmadi, birozdan keyin urinib ko'ring"
  }
}
```

Kodga qarab ish tuting, `message` ga emas — u o'zgarishi mumkin.

| HTTP | Kod | Nima qilish kerak |
|---|---|---|
| 400 | `invalid_amount` | summani tekshiring: butun son, chegarada |
| 400 | `invalid_request` | so'rov maydonlarini tekshiring |
| 401 | `unauthorized` | kalitni tekshiring; bloklangan bo'lsa adminga murojaat |
| 403 | `forbidden` | panel API sida: bu amalga ruxsatingiz yo'q |
| 404 | `not_found` | `invoice_id` yoki `external_id` noto'g'ri |
| 409 | `already_paid` | to'langan hisobni bekor qilib bo'lmaydi |
| 409 | `no_free_amount` | 2–3 soniyadan keyin qayta urinib ko'ring |
| 429 | `rate_limited` | so'rovlar tezligini kamaytiring |
| 500 | `internal_error` | qayta urinib ko'ring; takrorlansa adminga xabar bering |
| 503 | `no_active_card` | adminga xabar bering — karta o'chirilgan yoki limiti to'lgan |

### Qayta urinish tavsiyalari

| Kod | Qayta urinsa bo'ladimi |
|---|---|
| `no_free_amount` | **ha** — 2–3 soniyadan keyin |
| `rate_limited` | **ha** — kutib turib |
| `internal_error` | **ha** — orani ochib (exponential backoff) |
| `no_active_card` | **ha** — lekin admin aralashuvi kerak |
| `invalid_*`, `unauthorized`, `not_found` | **yo'q** — so'rovning o'zi noto'g'ri |

Qayta urinishda **o'sha** `external_id` ni ishlating — idempotentlik
ikki marta hisob yaratilishiga yo'l qo'ymaydi.

---

## 10. To'liq integratsiya misoli

```python
import os
import aiohttp

BASE = os.environ.get("CARDHOOK_URL", "http://127.0.0.1:8080")
KEY = os.environ["CARDHOOK_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}


class CardHookError(Exception):
    def __init__(self, code, message):
        super().__init__(f"{code}: {message}")
        self.code = code


async def create_invoice(session, order_id, amount_som, description=""):
    """Hisob yaratadi. Xato bo'lsa CardHookError."""
    payload = {
        "external_id": order_id,
        "amount": amount_som,
        "callback_url": "https://star.uz/cardhook/callback",
        "expires_in": 1800,
        "description": description,
    }
    async with session.post(
        f"{BASE}/v1/invoices", json=payload, headers=HEADERS
    ) as resp:
        data = await resp.json()
        if resp.status not in (200, 201):
            err = data.get("error", {})
            raise CardHookError(err.get("code"), err.get("message"))
        return data


async def get_invoice(session, invoice_id):
    async with session.get(
        f"{BASE}/v1/invoices/{invoice_id}", headers=HEADERS
    ) as resp:
        if resp.status == 404:
            return None
        return await resp.json()


async def cancel_invoice(session, invoice_id):
    async with session.post(
        f"{BASE}/v1/invoices/{invoice_id}/cancel", headers=HEADERS
    ) as resp:
        return await resp.json()


# --- Ishlatish ---

async def start_payment(order_id, amount):
    async with aiohttp.ClientSession() as session:
        invoice = await create_invoice(session, order_id, amount, "100 Stars")

    # Mijozga AYNAN pay_amount ni ko'rsating
    return (
        f"To'lov summasi: {invoice['pay_amount']:,} so'm\n".replace(",", " ")
        + f"Karta: {invoice['card']['number']}\n"
        + f"{invoice['card']['holder']}\n\n"
        + "Aynan shu summani o'tkazing - 1 so'm farq ham qabul qilinmaydi."
    )
```

---

## 11. Mijozga nima ko'rsatish kerak

Bu joy integratsiyaning eng muhim qismi. Noyob summa mexanizmi faqat
mijoz **aynan** o'sha summani o'tkazsagina ishlaydi.

**Ko'rsatish shart:**

- `pay_amount` — katta va aniq, `amount` emas
- karta raqami va egasining ismi
- "aynan shu summani o'tkazing" degan ogohlantirish
- qolgan vaqt (`expires_at` gacha)

**Namuna:**

```
To'lov summasi
24 017 so'm          <- pay_amount, amount emas

aynan shu summani o'tkazing
1 so'm farq ham qabul qilinmaydi

HUMO
9860 1701 1203 0268
ISM FAMILIYA

Qoldi: 29:45
```

Mijoz 24 000 o'tkazsa, to'lov **topilmaydi** va qo'lda hal qilishga
to'g'ri keladi. Shuning uchun summani yaxlitlash yoki "taxminan" deb
ko'rsatish mumkin emas.

---

## 12. Sinov

Hozircha alohida test muhiti yo'q. Integratsiyani shunday sinang:

1. Kichik summali hisob yarating (masalan 1 000 so'm)
2. O'z kartangizdan aynan `pay_amount` ni o'tkazing
3. Callback kelganini va buyurtma bajarilganini tekshiring
4. Bir xil `external_id` bilan qayta so'rov yuboring — `200` va o'sha
   hisob qaytishi kerak
5. Callback'ni qasddan `500` qaytaradigan qiling va qayta urinishlar
   ishlaganiga ishonch hosil qiling

Imzo tekshirishni alohida sinash uchun:

```python
import hashlib, hmac

secret = "sizning_secret"
ts = "1790000000"
body = b'{"invoice_id":"inv_test","status":"paid"}'

signature = "sha256=" + hmac.new(
    secret.encode(), ts.encode() + b"." + body, hashlib.sha256
).hexdigest()

print(signature)
```

---

## 13. Xavfsizlik

**Sizning tomoningizda:**

- `API_KEY` va `CALLBACK_SECRET` ni muhit o'zgaruvchisida saqlang,
  kodga yozmang, git'ga tushirmang
- callback imzosini **har doim** tekshiring
- `X-Timestamp` ni tekshiring (5 daqiqadan eski so'rovlarni rad eting)
- callback'ni faqat HTTPS orqali qabul qiling
- kalit oshkor bo'lsa panelda darhol yangilang

### Platformangiz nimani ko'radi

| Ko'radi | Ko'rmaydi |
|---|---|
| **Qabul kartasining to'liq raqami va egasining ismi** — hisob yaratish javobidagi `card` maydonida. Busiz mijozga qayerga pul o'tkazishni ayta olmaysiz. | Boshqa platformalarning hisoblari, to'lovlari, statistikasi |
| O'z hisoblaringiz va ularning holati | Kartaning kunlik limiti, bugungi aylanmasi |
| O'z callbacklaringiz | Bog'lanmagan (`orphan`) tushumlar |

Karta raqami sir emas: u mijozga baribir ko'rsatiladi. Sir bo'lgani —
`API_KEY` va `CALLBACK_SECRET`.

Kartalarni boshqarish (qo'shish, o'chirish, loyihaga biriktirish) va
noaniq tushumlarni qo'lda bog'lash **API orqali umuman mumkin emas** —
bular faqat operator panelida bajariladi. Shu sababli bitta platformaning
kaliti oshkor bo'lsa ham, u boshqa platformaning puliga yeta olmaydi va
kartani o'chirib to'lovlarni to'xtata olmaydi.

**CardHook tomonida:**

- API kalitlari bazada `sha256` hash ko'rinishida — ochiq matnda emas
- kalitlar `secrets.token_urlsafe(32)` bilan yaratiladi
- loglarda karta raqami to'liq yozilmaydi, faqat oxirgi 4 raqam
- so'rovlar oqimi cheklangan (nginx va servis ichida, ikki qatlam)
- barcha pul o'tishlari atomik va idempotent

---

## 14. Tez-tez so'raladigan savollar

**Mijoz noto'g'ri summa o'tkazsa nima bo'ladi?**
To'lov hech qanday hisobga bog'lanmaydi va admin panelidagi "Noaniq
to'lovlar" bo'limiga tushadi. Admin uni qo'lda kerakli hisobga bog'lashi
mumkin — shundan keyin sizga odatdagi callback keladi.

**Mijoz ikki marta to'lasa?**
Birinchi to'lov hisobni yopadi, ikkinchisi noaniq to'lovlarga tushadi va
admin ogohlantiriladi. CardHook avtomatik qaytarish qilmaydi.

**Hisob muddati tugagandan keyin mijoz to'lasa?**
To'lov noaniq to'lovlarga tushadi. Admin uni qo'lda bog'lashi mumkin.

**Bitta buyurtma uchun ikkita hisob yaratsam bo'ladimi?**
Ha, lekin har biriga boshqa `external_id` kerak. Eskisini `/cancel`
bilan yopish tavsiya etiladi — shunda uning summasi bo'shaydi.

**`pay_amount` ni o'zim tanlasam bo'ladimi?**
Yo'q. Noyoblikni CardHook boshqaradi — aks holda ikkita hisob bir xil
summa olishi va to'lovni bog'lab bo'lmay qolishi mumkin.

**Bir nechta karta ishlatsam bo'ladimi?**
Ha. Admin bir nechta karta qo'shishi mumkin, CardHook har hisob uchun
kunlik limiti to'lmagan kartani tanlaydi. Qaysi karta tanlanganini
javobdagi `card` maydonida ko'rasiz.

**Callback nechta marta keladi?**
Muvaffaqiyatli bo'lsa — bir marta. Javobingiz `2xx` bo'lmasa 6 martagacha.
Shuning uchun idempotentlik majburiy.

**Valyuta faqat so'mmi?**
Ha. Boshqa valyutalar qo'llab-quvvatlanmaydi.

---

## 15. Tez ma'lumotnoma

```
POST   /v1/invoices                     hisob yaratish
GET    /v1/invoices/{invoice_id}        hisobni olish
GET    /v1/invoices?external_id=...     o'z raqami bo'yicha
POST   /v1/invoices/{invoice_id}/cancel bekor qilish

Authorization: Bearer pbx_xxxxxxxx
Summalar: so'mda, butun son
Vaqtlar:  UTC, ISO 8601 (...Z)

Holatlar: pending | paid | expired | cancelled

Callback imzosi:
  HMAC_SHA256(callback_secret, timestamp + "." + raw_body)
  X-Signature: sha256=<hex>

Qayta urinish: darhol, 5s, 30s, 2m, 10m, 1h  (6 marta)
Rate limit:    60 so'rov / daqiqa
```

---

## 16. Integratsiya ro'yxati

Ishga tushirishdan oldin tekshiring:

- [ ] `API_KEY` va `CALLBACK_SECRET` muhit o'zgaruvchisida, kodda emas
- [ ] Mijozga `pay_amount` ko'rsatiladi (`amount` emas)
- [ ] "Aynan shu summani o'tkazing" ogohlantirishi bor
- [ ] Karta raqami va egasining ismi ko'rsatiladi
- [ ] Qolgan vaqt ko'rsatiladi
- [ ] `external_id` har buyurtma uchun noyob
- [ ] Callback imzosi tekshiriladi
- [ ] `X-Timestamp` tekshiriladi (replay himoyasi)
- [ ] Callback `invoice_id` bo'yicha idempotent
- [ ] Callback tez javob qaytaradi (og'ir ish fonga)
- [ ] Callback `2xx` qaytaradi
- [ ] Xatolar `code` bo'yicha ishlanadi
- [ ] `no_free_amount` da qayta urinish bor
- [ ] Zaxira sifatida `GET /v1/invoices/{id}` tekshiruvi bor

---

*CardHook v1 · Savollar bo'lsa admin bilan bog'laning.*
