# GraceGames API Kullanım Kılavuzu

> Hedef kitle: SDK geliştiricileri (Unity, native) ve backoffice geliştiricileri.
>
> **İki ayrı API var:**
> - **SDK API** — `https://api.gracegames.com` — oyun client'lar için (X-API-Key)
> - **Admin API** — `https://admin-api.gracegames.com` — backoffice için (JWT)
>
> Endpoint'ler ait oldukları API'de yaşar; cross-call yoktur (`/auth/*` admin-api'da, `/collect/*` api'da).

## İçerik

- [1. Authentication](#1-authentication)
- [2. SDK Endpoint'leri (X-API-Key)](#2-sdk-endpointleri-x-api-key)
  - [2.1 Connect](#21-post-collectconnect)
  - [2.2 Player](#22-post-collectplayer)
  - [2.3 Session](#23-session-endpointleri)
  - [2.4 Event](#24-post-collectevent)
  - [2.5 Remote Config](#25-get-config)
  - [2.6 State (Wallet + Progress)](#26-state---wallet--progress)
  - [2.7 IAP (Receipt validation)](#27-iap)
- [3. Admin Endpoint'leri (JWT)](#3-admin-endpointleri-jwt)
- [4. Hata Kodları](#4-hata-kodları)
- [5. Rate Limit & CORS](#5-rate-limit--cors)
- [6. Örnek Akış (Unity SDK)](#6-örnek-akış-unity-sdk)

---

## 1. Authentication

İki ayrı auth mekanizması, iki ayrı domain:

| Tip | Domain | Kim Kullanır | Header |
|---|---|---|---|
| **API Key** | `api.gracegames.com` | SDK / oyun client | `X-API-Key: <api_key>` |
| **JWT** | `admin-api.gracegames.com` | Admin (backoffice) | `Authorization: Bearer <access_token>` |

API Key her oyun için ayrı oluşturulur (SHA-256 hash'lenip saklanır), backoffice'ten **API Keys** sayfasından üretilir. SDK her isteğe bu key'i koyar; backend key'den `game_id`'yi resolve eder.

JWT kullanıcı bazlıdır (admin login). Access token 24 saat, refresh token 30 gün geçerlidir; refresh ile rotate edilir.

---

## 2. SDK Endpoint'leri (X-API-Key)

> Bütün SDK istekleri `X-API-Key` header'ı ister. Header eksikse veya geçersizse `401`.
> Body'ler JSON, response'lar JSON.

### 2.1 `POST /collect/connect`

Bağlantı testi. Genelde SDK init'inde bir kez çağrılır.

**Request body:** boş (`{}`)

**Response 200:**
```json
{
  "success": true,
  "game_id": "uuid",
  "game_name": "Water Sort",
  "game_slug": "water-sort"
}
```

---

### 2.2 `POST /collect/player`

Player upsert (insert veya update). Aynı `(game_id, external_id)` ikilisi varsa update'lenir, yoksa yeni kayıt oluşturulur. Affiliate alanları (`campaign_id`, `banner_id`) **first-touch** korunur — sonradan değişmez.

**Request body:**
```json
{
  "external_id": "device-uuid-or-stable-id",     // ZORUNLU
  "username": "Caner",                            // opsiyonel
  "country": "TR",                                // ISO 3166-1 alpha-2, ops.
  "platform": "android",                          // android|ios|web|...
  "app_version": "1.4.2",                         // ops.
  "ip": "1.2.3.4",                                // ops. (server da yakalar)
  "campaign_code": "fb_q4_promo",                 // ops. — first-touch
  "banner_code": "creative_v3",                   // ops. — first-touch
  "extra": { "abi": "arm64", "ram_mb": 6144 }     // ops. jsonb merge
}
```

**Response 200:**
```json
{
  "player_id": "uuid",
  "external_id": "...",
  "first_seen": true     // ilk kez mi oluşturuldu
}
```

**Notlar:**
- `extra` field'ı server'da JSONB merge edilir (`extra || EXCLUDED.extra`).
- `campaign_code` + `banner_code` server'da ID'ye çevrilir; geçersiz code'lar sessizce atlanır.

---

### 2.3 Session endpoint'leri

#### `POST /collect/session/start`
**Request:**
```json
{
  "player_id": "uuid",
  "extra": { "level": 5, "scene": "MainMenu" }   // ops.
}
```
**Response:** `{ "session_id": "uuid" }`

#### `POST /collect/session/end`
**Request:**
```json
{
  "session_id": "uuid",
  "duration_seconds": 245,                        // ops. — yoksa server NOW()-started_at hesaplar
  "extra": { "ended_reason": "user_quit" }
}
```
**Response:** `{ "ok": true }`

#### `POST /collect/session/heartbeat`
**Her 30-60 saniyede bir** çağrılması beklenir. Heartbeat gelmezse stale session cleanup task'ı session'ı 1 dk sonra otomatik kapatır.
```json
{ "session_id": "uuid" }
```
**Response:** `{ "ok": true }`

---

### 2.4 `POST /collect/event`

Özel event logla (`level_complete`, `purchase_attempt`, vb.).

**Request:**
```json
{
  "event_type": "level_complete",       // ZORUNLU, snake_case önerilir
  "player_id": "uuid",                  // ZORUNLU
  "session_id": "uuid",                 // ops. ama önerilir
  "payload": {                          // ops. jsonb
    "level": 12,
    "stars": 3,
    "duration_s": 87
  }
}
```
**Response:** `201 { "ok": true }`

---

### 2.5 `GET /config/`

Oyunun config'lerini ve aktif experiment variant'larını döner.

**Query params:**
- `player_id` (ops.) — verildiğinde aktif experiment'lerin player'a atanmış variant'ları uygulanır ve override'lar config'lere yedirilir.

**Response 200:**
```json
{
  "etag": "16-char-hash",                       // değişiklik tespiti
  "configs": [
    { "key": "coin_reward",      "value": 50,    "type": "int"    },
    { "key": "tutorial_enabled", "value": true,  "type": "bool"   },
    { "key": "level_pack_url",   "value": "...", "type": "string" }
  ],
  "experiments": {
    "difficulty_test": "harder",                // experiment_key: variant_name
    "tutorial_v2":     "control"
  }
}
```

**Tip yapısı:**
- `string`, `int`, `float`, `bool`, `json` desteklenir.
- Aynı oyun içinde key unique.

**Önerilen SDK akışı:** uygulama açılışında bir kez çek + `etag`'i sakla, sonra app foreground'a geldiğinde tekrar çağır. Network yoksa SQLite cache'ten oku.

---

### 2.6 State — Wallet + Progress

Local SQLite'taki wallet ve progress'i sunucuya backup alıp gerektiğinde restore etmek için.

#### `POST /state/push`
**Request:**
```json
{
  "player_id": "uuid",
  "wallet": [
    { "currency": "coin", "balance": 1500 },
    { "currency": "gem",  "balance": 12 }
  ],
  "progress": [
    { "key": "current_level", "value": 27 },
    { "key": "stars",         "value": { "1": 3, "2": 2, "3": 3 } }
  ],
  "transactions": [
    {
      "client_seq": 1234,                       // monotonic, idempotency anahtarı
      "currency": "coin",
      "delta": -50,                             // negatif=harcama, pozitif=kazanç
      "source": "buy_hint",
      "balance_after": 1450,
      "occurred_at": 1730000000                 // unix saniye
    }
  ]
}
```
**Response:**
```json
{ "ok": true, "wallet_count": 2, "progress_count": 2, "transactions_count": 1 }
```

**Idempotency:** `(player_id, client_seq)` UNIQUE; aynı transaction tekrar gelirse atlanır. SDK kendi local seq counter'ını tutmalı.

#### `GET /state/pull/{player_id}`
Cihaz değişimi / login senaryosunda server'daki state'i restore et.

**Response:**
```json
{
  "wallet":   [ { "currency": "coin", "balance": 1500, "updated_at": "..." } ],
  "progress": [ { "key": "current_level", "value": 27, "updated_at": "..." } ],
  "last_pushed_at": "2026-04-28T10:15:00Z"
}
```

> ⚠️ Player game'e ait değilse `403`. SDK player'ı `/collect/player` ile register ettikten sonra çağırmalı.

---

### 2.7 IAP

#### `GET /iap/products`
Bu oyunun aktif ürünlerini listeler (SKU + grant info).

**Response:**
```json
[
  {
    "id": "uuid",
    "sku": "coin_pack_100",
    "name": "100 Coins",
    "description": "Starter pack",
    "grant_currency": "coin",
    "grant_amount": 100,
    "price_usd": "0.99"
  }
]
```

#### `POST /iap/validate`
Satın alım sonrası receipt'i server'a gönder, server validate eder ve **server-authoritative** olarak wallet'a coin ekler. Client cevaba göre kendi wallet'ını günceller.

**Request:**
```json
{
  "player_id": "uuid",
  "sku": "coin_pack_100",
  "store": "google",                  // "google" | "apple" | "stub" (dev)
  "receipt": "<store-specific-receipt-string>",
  "store_tx_id": "GPA.1234..."        // ops. — yoksa server hesaplar
}
```

**Response (success):**
```json
{
  "ok": true,
  "duplicate": false,                 // aynı tx_id daha önce gelmiş mi
  "transaction_id": "uuid",
  "granted": { "currency": "coin", "amount": 100 }
}
```

**Idempotent:** aynı `(store, store_tx_id)` ile tekrar gelirse mevcut transaction döner (`duplicate: true`), tekrar coin verilmez.

**Hatalar:**
- `400 Receipt validation failed` — geçersiz receipt
- `404 Product not found` — SKU yok
- `403 Player not in this game`

> ⚠️ Şu an Apple/Google receipt validation **stub**. Production'da Apple `verifyReceipt` / App Store Server API ve Google Play Developer API entegrasyonu yapılacak (Phase 5'in production hardening adımı).

---

## 3. Admin Endpoint'leri (JWT)

> Admin endpoint'leri backoffice'in kullandığı endpoint'lerdir. SDK developer'ı kullanmaz; backoffice geliştiricisi referans için.

| Grup | Path | Açıklama |
|---|---|---|
| Auth | `POST /auth/login`, `/refresh`, `/logout`, `GET /auth/me` | JWT auth |
| Admins | `/admins/*` | Admin user CRUD |
| Games | `/games/*` | Oyun tanımı CRUD |
| Platforms | `/platforms/*` | Platform tanımı (android/ios/web) |
| API Keys | `/api-keys/*` | Game-scoped API key oluşturma |
| Players | `/players/?...`, `/players/{id}`, `/players/filters` | Player listesi (filtreler), detay, filter values |
| Sessions | `/sessions/?...`, `/sessions/{id}`, `/sessions/{id}/events` | Session listesi, detay, event listesi |
| Campaigns | `/campaigns/*`, `/campaigns/{id}/banners` | Affiliate kampanyaları |
| Banners | `/campaigns/banners/all` | Tüm banner'lar (cross-campaign) |
| Configs | `/config/{game_id}` (GET/POST/PUT/DELETE) | Remote config CRUD |
| Experiments | `/experiments/*` | A/B test CRUD + variant CRUD |
| IAP | `/iap/{game_id}` (GET/POST/PUT/DELETE), `/iap/transactions/{player_id}` | IAP product CRUD + tx listesi |
| State (admin) | `/state/{player_id}`, `/state/{player_id}/transactions` | Player wallet/progress read-only |
| Alerts | `/alerts/*` | Graylog ingest + alert listesi |
| Agents | `/agents/*` | Agent runner monitoring |
| Stats | `/stats/*` | Dashboard sayıları |

---

## 4. Hata Kodları

Backend FastAPI standartını kullanır:

| Status | Anlam | Tipik Sebep |
|---|---|---|
| `400` | Bad Request | Geçersiz body, validation error (Pydantic) |
| `401` | Unauthorized | API Key / JWT eksik veya geçersiz |
| `403` | Forbidden | Player başka oyuna ait, role yetmedi |
| `404` | Not Found | ID yok |
| `409` / `400` | Conflict | Duplicate key (örn. SKU zaten var) |
| `422` | Unprocessable | Body field eksik/yanlış tip |
| `429` | Too Many Requests | Rate limit (`/auth/login` 5/min) |
| `500` | Server Error | Beklenmedik hata |

**Hata response formatı:**
```json
{ "detail": "Insan-okur açıklama" }
```
Veya validation hatasında array:
```json
{ "detail": [ { "type": "...", "loc": [...], "msg": "...", "input": {...} } ] }
```

---

## 5. Rate Limit & CORS

- `/auth/login`: dakikada **5 attempt** (brute force koruması).
- Diğer endpoint'lerde şu an rate limit yok.
- CORS: `bo.gracegames.com` allow listed (production hardening yapılacak — ClickUp #cors-task).

---

## 6. Örnek Akış (Unity SDK)

Tipik bir oyun açılışı:

```
1. SDK Init
   POST /collect/connect          → game_id, game_slug
   GET  /config/?player_id=null   → varsayılan config'ler (player yok henüz)

2. Player oluştur / load
   POST /collect/player           → player_id (first_seen veya değil)

3. Session başlat
   POST /collect/session/start    → session_id

4. (varsa) State restore
   GET  /state/pull/{player_id}   → wallet + progress

5. (varsa) Config refresh — bu sefer player_id ile
   GET  /config/?player_id=...    → variant override'ları dahil

6. Oyun süresince:
   - Her 30sn:                    POST /collect/session/heartbeat
   - Önemli olaylar:              POST /collect/event
   - 5 dakikada bir veya app pause:
                                   POST /state/push (wallet + progress diff)

7. IAP satın alım:
   POST /iap/validate             → server grant + idempotent
   (client kendi wallet'ını response'a göre senkronlar)

8. Session bitir:
   POST /collect/session/end      → duration_seconds (ops.)
```

### Minimal C# örneği

```csharp
// İstek atan tek metod (Unity SDK içinde JsonHelper + UnityWebRequest sarmalı)
async Task<T> PostAsync<T>(string path, object body) {
    using var req = UnityWebRequest.Post(BaseUrl + path, JsonHelper.Serialize(body), "application/json");
    req.SetRequestHeader("X-API-Key", _apiKey);
    var op = req.SendWebRequest();
    while (!op.isDone) await Task.Yield();
    if (req.result != UnityWebRequest.Result.Success) throw new Exception(req.error);
    return JsonHelper.Deserialize<T>(req.downloadHandler.text);
}

// Player upsert
var player = await PostAsync<PlayerResponse>("/collect/player", new {
    external_id  = SystemInfo.deviceUniqueIdentifier,
    username     = "Guest_" + RandomSuffix(),
    country      = SystemRegion(),
    platform     = Application.platform.ToString(),
    app_version  = Application.version
});
```

---

## Ek: Geliştirme tarafı

- **Local backend run:** `cd gracegames-backend && source venv/bin/activate && uvicorn app.main:app --reload`
- **DB tunnel:** `ssh -i ~/.ssh/id_ed25519 -L 5433:10.0.0.3:5432 root@94.130.181.122 -N`
- **Backend repo:** https://github.com/GraceGameStudios/gracegames-backend
- **Backoffice repo:** https://github.com/GraceGameStudios/gracegames-backoffice
- **SDK repo:** https://github.com/GraceGameStudios/gracegames-unity-sdk

Sorular için: `ctosuncuk@gracegames.com` (Caner — backend), `herzi@gracegames.com` (Hasan — Unity SDK).
