In-App Purchase (IAP) ✓ Canlı

Tenant'tan tek-seferlik ödeme alın (premium kilit, kredi paketi vb.). Akış: purchases/create ile checkout başlatın → tenant Stripe Checkout'ta öder → purchase.granted webhook'u düşer (kilidi açın) → purchases/get ile kesin doğrulayın.

← Para Kazanma (Komisyon) · ilgili: Payout. Ortak kurallar (base, auth, hata zarfı): API Uçları.

Akış & durum makinesi

Mutlu yol: pending → paid → granted. Para iadesi (refund) hem paid hem granted durumundan mümkündür → refunded. Ödeme alınmadan checkout düşerse pending → failed | canceled.

durum makinesi
pending ──öde──► paid ──grant──► granted
   │                │              │
   │ failed/canceled│ refund       │ refund
   ▼                ▼              ▼
failed/canceled  refunded       refunded
statusAnlamTerminal mi?
pendingCheckout başlatıldı, ödeme henüz tamamlanmadı.hayır
paidTenant ödedi; grant işleniyor.hayır (refund olabilir)
grantedHak teslim edildi (kilidi açabilirsiniz).hayır (refund olabilir)
refundedİade edildi (paid veya granted'den).evet
failedÖdeme başarısız.evet
canceledTenant checkout'u iptal etti.evet
Grant durumunu webhook'a bağlamayın: kesin gerçek için her zaman purchases/get ile doğrulayın (aşağıda).

Scope'lar

IAP iki scope kullanır. PII değildir — finansal alanlar (net / developerShare / Stripe ID) hiçbir uçtan sızmaz.

ScopeNe için
purchases:writeCheckout başlat (purchases/create). Tenant UI'da onaylar; OAuth Connect şart.
purchases:readKendi satın almalarınızı okuyun (purchases/get, purchases/list) + purchase.granted event'i.

Checkout başlat — POST /plugin-api/purchases/create

Method / yolPOST /plugin-api/purchases/create
Authinstall API key — Authorization: Bearer serverId.pluginId.secret (token)
Scopepurchases:write
Rate limitwrite kovası — Limitler
İstek
POST {RESTOMENUM_BASE}/plugin-api/purchases/create
Authorization: Bearer <apiKey>     // install API key: serverId.pluginId.secret
Content-Type: application/json

{
  "amount": 10000,                  // zorunlu — GROSS kuruş (100 = 1,00₺)
  "productKey": "premium_unlock",   // ops — opaque SKU (sizin tarafınızda anlamlı)
  "description": "Premium kilit",   // ops
  "idempotencyKey": "buy-9af2",     // ops AMA ÖNERİLİR (retry'da çift checkout engeller)
  "successUrl": "https://app.restomenum.com/...", // ops (https + restomenum.com/.app domaini)
  "cancelUrl": "https://app.restomenum.com/..."   // ops (aynı kural)
}

İstek alanları

AlanTipZorunluAçıklama
amountintGROSS tutar kuruş cinsinden. min 100 (1,00₺), max 5.000.000 (50.000,00₺). Aralık dışı → plugin.iap.badAmount.
productKeystringOpaque SKU — sizin tarafınızda anlamlı (Restomenum yorumlamaz).
descriptionstringTenant'a gösterilecek kısa açıklama.
idempotencyKeystringKendi benzersiz anahtarınız. Retry güvenli (aşağıdaki davranışa bkz).
successUrlstring (https)Ödeme sonrası dönüş. https + hostname restomenum.com/.app veya alt alan adı olmalı; PATH doğrulanmaz. Geçersizse default kullanılır.
cancelUrlstring (https)İptal dönüşü — aynı kural (geçersiz → default).
Yanıt
// başarı (200)
{
  "success": true,
  "data": {
    "url": "https://checkout.stripe.com/c/pay/...",  // tenant'ı buraya yönlendir
    "sessionId": "cs_test_...",                       // yalnız referans
    "purchaseId": "9b1f2c3d"                          // KANONİK — sakla, get/list ile bununla sorgula
  }
}

// hata
{ "success": false, "message": "<kod>" }

data.purchaseId kanonik kimliktir — saklayın; get/list ile bununla sorgulayın. sessionId yalnız referanstır.

Hatalar

HTTPmessageAnlam
403plugin.iap.disabledIAP bu eklenti için kapalı.
403plugin.billing.disabledFaturalama kapalı.
403plugin.scope.deniedpurchases:write onaylı değil.
403plugin.billing.connectFirstÖnce OAuth Connect gerekli.
409plugin.iap.alreadyPurchasedTamamlanmış satın alma var (idempotencyKey ile, aşağı bkz).
400plugin.iap.badAmountamount aralık dışı (min 100, max 5.000.000).
400plugin.iap.notFoundİlgili IAP kaydı bulunamadı.
400plugin.billing.notInstalledFaturalama kurulmamış.
500Sunucu hatası.
idempotencyKey davranışı (retry güvenli):
  • Aynı anahtarla tamamlanmış (paid/granted) satın alma → 409 plugin.iap.alreadyPurchased (url YOK).
  • Hâlâ pending ise → aynı purchaseId ile taze url (200).
  • Çift Stripe session açılmaz; tekrar denemek güvenlidir.

Satın alma detayı — GET /plugin-api/purchases/get

Method / yolGET /plugin-api/purchases/get?purchaseId=
Scopepurchases:read
Rate limitcallback kovası — Limitler
purchaseId≤200 karakter; / içeremez.
İstek
GET {RESTOMENUM_BASE}/plugin-api/purchases/get?purchaseId=9b1f2c3d
Authorization: Bearer <apiKey>
Yanıt (granted)
// başarı — yalnız allowlist alanları döner (toPublic)
{
  "success": true,
  "data": {
    "purchaseId": "9b1f2c3d",
    "status": "granted",
    "type": "one_time",
    "productKey": "premium_unlock",
    "description": "Premium kilit",
    "amount": 10000,
    "currency": "try",
    "createdAt": 1718200000000,
    "paidAt": 1718200025000,
    "grantedAt": 1718200030000,
    "refundedAt": null
  }
}
Allowlist (toPublic). Yalnız şu alanlar döner: purchaseId, status, type, productKey, description, amount, currency, createdAt, paidAt, grantedAt, refundedAt. Finansal/iç alanlar (net, developerShare, platformShare, Stripe ID, developerId, testMode) ASLA sızmaz.

Hatalar

HTTPmessageAnlam
403plugin.scope.deniedpurchases:read onaylı değil.
404plugin.purchases.notFoundKayıt yok veya sahibi farklı.
400plugin.purchases.missingParamspurchaseId boş, >200 karakter veya / içeriyor.

Satın almaları listele — GET /plugin-api/purchases/list

Method / yolGET /plugin-api/purchases/list
Scopepurchases:read
limitops — default 50, max 100.
SıralamaEn yeni önce (createdAt desc).
KapsamYalnız çağıran install'ın (serverId + pluginId) kayıtları.
İstek
GET {RESTOMENUM_BASE}/plugin-api/purchases/list?limit=50
Authorization: Bearer <apiKey>
Yanıt
// başarı — en yeni önce (createdAt desc); yalnız çağıran install'ın kayıtları
{
  "success": true,
  "data": [
    { "purchaseId": "9b1f2c3d", "status": "granted",  "amount": 10000, "currency": "try", "createdAt": 1718200000000, ... },
    { "purchaseId": "7a2e1b0c", "status": "refunded", "amount": 20000, "currency": "try", "createdAt": 1718100000000, ... }
  ]
}

Her kayıt aynı allowlist'le döner (get ile aynı alanlar).

Hatalar

HTTPmessageAnlam
403plugin.scope.deniedpurchases:read onaylı değil.
400Geçersiz limit.

purchase.granted webhook

Satın alma teslim edilince purchase.granted event'i webhook'unuza düşer → kilidi açın. Scope purchases:read; PII yok. Standart imzalı pipeline (SSRF guard, HMAC, retry) ile gelir — imza doğrulama ve /webhook alıcısı.

purchase.granted (envelope)
{
  "id": "9b1f2c3d_granted",
  "type": "purchase.granted",
  "version": "1",
  "tenantId": "<tenantId>",
  "occurredAt": 1718200030000,
  "data": {
    "purchaseId": "9b1f2c3d",
    "pluginId": "plugin_abc",
    "type": "one_time",
    "productKey": "premium_unlock",
    "amount": 10000,
    "currency": "try"
  }
}

data alanları: purchaseId, pluginId, type, productKey, amount (kuruş), currency. Katalog: Event Kataloğu.

Webhook = kolaylık, purchases/get = KESİN GERÇEK.
  • Webhook teslimi garanti değildir; grant durumunu webhook'a bağlamayın.
  • Her zaman purchases/get ile authoritatively doğrulayın — böylece paid'de takılı kalmazsınız.
Idempotency / dedup. Event id deterministiktir: <purchaseId>_granted (örn. 9b1f2c3d_granted). Teslim at-least-once'tur → aynı id tekrar gelebilir; id ile dedup edin ve işleminizi idempotent kurun. Event satın alma başına bir kez emit edilir (durum makinesi guard'ı).