Geliştirici / APILisans & Ödeme API

Lisans & Ödeme API

Bu sayfa, para ile ilgili her şeyin entegratör referansıdır: katalog fiyatlarını okuma, işletmenin lisans ve ürün durumunu sorgulama, checkout rayından ödeme alma, faturaları okuma ve yenileme döngüsünü izleme. API Temelleri sayfasındaki ortak kuralların (taban URL, auth realm’leri, şube kapsamı, hata zarfı, idempotency, rate-limit’ler) üzerine kurulur.

Model: çekirdek ürün süresiz ücretsizdir. Ücretli yetenekler yıllık lisans (license_annual) ön koşuluyla, katalogdan tek tek satın alınır. Tüm tutarlar TRY ve KDV dahildir; tahsilat yalnızca PayTR üzerinden, yalnızca TRY olarak yapılır. Plan, tier, paket, deneme süresi ve otomatik yenileme yoktur.

Kimlik & roller

Bu uçlar Personel JWT realm’ini kullanır (Authorization: Bearer <jwt> — bkz. API Temelleri). Rol gereksinimleri uca göre değişir:

YetenekGerekli rol
Katalog fiyat listesini okumaPublic (@Public, kimliksiz)
Lisans durumunu / faturaları okumaKimliği doğrulanmış herhangi bir kullanıcı
Sepeti fiyatlandırma (/checkout/quote)Kimliği doğrulanmış herhangi bir kullanıcı
Checkout intent / confirmADMIN, MANAGER
Ürün iptaliYalnız ADMIN
⚠️

POST /v1/checkout/quote hiçbir yazma yapmaz — hipotetik bir sepeti fiyatlandırır — bu yüzden rol kapısı yoktur. Durum yaratan ilk adım /checkout/start, para hareketi yaratan ilk adım /checkout/intent’tir; ikisi de ADMIN/MANAGER ister.

Uç özeti

YöntemYolRealm / rolAmaç
GET/api/v1/catalog/pricingPublicYayınlanmış katalog + fiyatlar (?locale=tr)
GET/api/v1/me/licensingPersonel JWTLisans durumu, sahip olunanlar, kontörler, yenileme, teklifler, satın alınabilirlik
GET/api/v1/me/invoicesPersonel JWTBu tenant’ın kalem kalem à-la-carte faturaları
GET/api/v1/entitlements/mePersonel JWTKatlanmış yetki seti
POST/api/v1/checkout/quotePersonel JWTKarma sepeti fiyatlandır (yazma yok)
POST/api/v1/checkout/startADMIN, MANAGERYönlendirme öncesi teklifi sabitle (yeniden fiyatlandırma)
POST/api/v1/checkout/intentADMIN, MANAGERPayTR iframe token’ı + paymentRef üret
POST/api/v1/checkout/confirmADMIN, MANAGERYerleşmiş bir intent’i provizyona çevir (idempotent)
POST/api/webhooks/paytrPayTR (HMAC + IP allowlist)Ödeme sonucu geri-çağrısı
DELETE/api/v1/marketplace/addons/:idADMINSahip olunan bir ürünü iptal et
GET/api/subscriptions/plansPublicKalıcı olarak boş dizi (aşağıya bakın)
GET/api/subscriptions/tenant/invoicesADMIN, MANAGEREski abonelik faturaları arşivi (sayfalı)
GET/api/invoices/:invoiceNumberADMIN, MANAGERTek eski fatura (/download ile PDF)
⚠️

GET /api/subscriptions/plans hâlâ @Public olarak yönlendirmede duruyor ama kalıcı olarak [] döner: uç yalnızca isActive && isPublic paketleri listeler ve v3.3.0 migrasyonu (20260811120000_free_core) her subscription_plans satırını isActive=false, isPublic=false yaptı. Satırlar yalnızca subscriptions.planId Restrict FK’sı ve VUK’un saklamayı zorunlu kıldığı eski faturalar için duruyor. Fiyat listesi için GET /v1/catalog/pricing kullanın.

Fiyat listesini okuma

Katalog herkese açıktır; pazarlama sayfaları da bunu okur, böylece süperadmin panelinden yapılan bir fiyat değişikliği sitede eski fiyatın kalmasına yol açamaz.

curl "https://app.hummytummy.com/api/v1/catalog/pricing?locale=tr"

Her ürün için dönen alanlar:

AlanAçıklama
codeDeğişmez ürün kodu (ör. module_inventory)
name / descriptionlocale için yerelleştirilmiş metin (yoksa TR fallback)
kindlicense | module | integration | capacity | credit | service
billingannual | oneTime
priceCentsKDV dahil kuruş — tam yıllık liste fiyatı
currencyHer zaman TRY
creditKind / creditUnitsYalnızca kontör paketlerinde
requiresLicenseKullanmak için canlı lisans gerekip gerekmediği
sortOrderVitrin sıralaması

Ürün kodlarının verdiği yetkiler için Yetki Matrisi’ne bakın.

Lisans durumu ve sahip olunanlar

GET /v1/me/licensing, SPA’nın lisans ekranını tek istekte çizmesini sağlar:

curl https://app.hummytummy.com/api/v1/me/licensing \
  -H "Authorization: Bearer $JWT"
Alanİçerik
entitlementsfeatures, limits, integrations, computedAt
licensestatus, anchorAt, anniversaryAt, daysRemaining
creditsKontör türü → kalan bakiye
ownedSahip olunan her satır: code, kind, quantity, pendingQuantity, status, periodEnd, chargedCents, renewalCents, origin
renewalAçık yenileme döngüsü: cycleId, anniversaryAt, graceEndsAt, totalCents, daysLeft
offersHer grant anahtarı → onu açan en ucuz ürün, bu tenant için bugün fiyatlanmış hâliyle
purchasabilityÜrün kodu → { ok } veya { ok: false, reason, message }

license.status dört değerden biridir:

DeğerAnlamı
noneHiç lisans alınmamış
activeLisans canlı
graceLisans satırı past_due — ödemesiz dönem sürüyor
expiredYıl dönümü geçmiş, ödenmemiş

offers ve purchasability, checkout’un kullandığı aynı fonksiyonlardan üretilir (LicensingService.price, evaluatePurchasability). Bu yüzden vitrinde gördüğünüz fiyat ile tahsil edilen fiyat, ve “Satın al” butonu ile checkout’un kabulü birbirinden ayrışamaz.

Yıl dönümü ve orantılı fiyatlama

Lisansın alındığı gün, hesabın değişmez yıl dönümü olur (tenant yerel takvim tarihi; varsayılan Europe/Istanbul). Sonradan alınan her yıllık kalem, yıl dönümüne kalan gün kadar orantılanır — böylece tüm hesap tek tarihte, tek kalemli faturayla yenilenir.

ModNe zamanFiyat
fullKalan gün = döngü günü (ilk alım veya tam yıl dönümünde alım)Tam liste fiyatı
proratedYıl dönümüne 14 günden fazla varliste × kalanGün / döngüGünü
rollForwardYıl dönümüne ≤ 14 gün kaldıKalan gün + bir sonraki tam döngü
  • Yuvarlama birim başına yapılır, sonra adetle çarpılır: unitCents × qty === subtotalCents daima birebir tutar.
  • Hiçbir ücretli satır ₺1’in altına inmez (MIN_LINE_CENTS = 100); gerçekten ücretsiz bir kalem ise 0 kalır.
  • Döngü uzunluğu 365 veya 366 olarak hesaplanır, sabit değildir; 29 Şubat yıl dönümü artık olmayan yıllarda 28 Şubat’a sıkıştırılır.

14 günlük eşik bilinçli: yıl dönümüne 2 gün kala alınan ₺990’lık bir modül ₺5,42 tutar, 48 saat sonra yenileme sepetine düşer ve PayTR’ın minimum tahsilat eşiğinin altında kalırdı — satış değil, destek talebi üretirdi.

KDV dahil fiyatlandırma içi

Katalogdaki tüm fiyatlar brüt (KDV dahil) tutarlardır ve müşteriden tam olarak bu tutar tahsil edilir. Quote, KDV’yi brütün içinden çıkarır; asla üzerine eklemez:

netCents = round(brütSatırToplamı / (1 + oran))
taxCents = brütSatırToplamı - netCents

TRY sepetlerde oran %20’dir. Quote yanıtında subtotalCents net, taxCents içerideki KDV, totalCents ise tahsil edilecek brüt tutardır (donanım varsa kargo dahil).

Checkout rayı

Bir ürünü satın almanın tek yolu checkout (PayTR) rayıdır. Ücretli hiçbir kalem ödeme kanıtı (paymentRef) olmadan provizyonlanmaz.

Sepeti fiyatlandırın

curl -X POST https://app.hummytummy.com/api/v1/checkout/quote \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "type": "addon", "code": "license_annual", "qty": 1 },
      { "type": "addon", "code": "module_inventory", "qty": 1 },
      { "type": "addon", "code": "extra_branch", "qty": 2 }
    ]
  }'

Sepet kalemi tipleri: addon (katalog ürünü — code), hardware (sku), service (code). Eski plan tipi DTO’da hâlâ kabul ediliyor ama fiyatlayıcı onun için hiçbir satır üretmez. Sepette en fazla 50 kalem, kalem başına en fazla 999 adet olabilir.

Ödeme niyeti (intent) oluşturun

curl -X POST https://app.hummytummy.com/api/v1/checkout/intent \
  -H "Authorization: Bearer $ADMIN_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "cart": { "items": [ { "type": "addon", "code": "license_annual", "qty": 1 } ] },
    "buyer": {
      "email": "[email protected]",
      "name": "Ali Veli",
      "phone": "+905551112233",
      "address": "..."
    },
    "returnUrl": "https://app.hummytummy.com/checkout/done",
    "acceptedDocumentIds": ["<KVKK>", "<MESAFELI_SATIS>", "<IADE>"]
  }'
AlanZorunluNot
cartEvetEn az 1 kalem
buyerEvetPayTR’ın gördüğü ve fraud skorlamasına giren bilgiler; telefon E.164’e normalize edilir
acceptedDocumentIdsEvetTam 3 adet: KVKK, Mesafeli Satış, İade Politikası. /legal/documents/:kind/current ucundan alınır
returnUrlHayırMutlak http(s) URL olmalı (açık yönlendirme koruması)
branchIdHayırDonanım gönderimi için tenant’a ait aktif şube
referralCodeHayırIntent anında çözülüp dondurulur

Yanıt: { paymentRef, iframeToken, paymentLink }. paymentRef CK-<uuid7> biçimindedir.

Sunucu bu adımda sırasıyla: demo tenant’ı bloklar, rıza kayıtlarını yazar, her addon satırını satın alınabilirlik kapısından geçirir, fiyatlama anını (pricedAt) dondurup sepeti yeniden fiyatlandırır (istemci toplamına asla güvenmez), donanım stoklarını ürün bazında toplayıp kontrol eder ve sepeti bir CheckoutIntent satırına dondurur.

PayTR’da ödeyin

Buyer, PayTR hosted iframe’inde öder. Sepetin her satırı ayrı bir kalem olarak gösterilir (ör. “Stok & Maliyet Yönetimi (yıllık)”).

Webhook ile yerleşim (settlement)

PayTR geri-çağrısı yalnızca merchant_oid + total_amount taşır. Dispatcher CK- önekini görüp çağrıyı CheckoutSettlementService’e yönlendirir; orada CheckoutIntent bulunur, dondurulmuş sepet ve pricedAt okunur ve CheckoutService.confirmAndProvision çalışır.

Provizyon

Her katalog satırı için TenantMarketplaceService.purchase() yerleşmiş paymentRef ile ve checkout’un transaction’ı içinde çağrılır: TenantAddOn satırı oluşur (ya da lapsed bir satır yerinde yeniden etkinleşir), outbox’a AddOnPurchased yazılır, kontör paketleri CreditLot mint eder. Entitlement projektörü olayı tüketip ürünün grants haritasını tenant’ın hak setine katar.

⚠️

Yerleşimde sepet pricedAt ile yeniden fiyatlandırılır ve toplam 1 kuruştan fazla kaydıysa provizyon reddedilir. Bu yüzden gece yarısını geçen bir intent bir gün ucuza yeniden fiyatlanıp müşteriyi ortada bırakmaz; tolerans yalnızca gerçek katalog fiyat değişikliklerini yakalar.

Satın alınabilirlik kapısı (tahsilat öncesi)

Her addon satırı, ödeme alınmadan önce kontrol edilir. Reddedilen satır 409 ile döner ve sepetin tamamı durur — hiçbir intent satırı yazılmaz, PayTR çağrılmaz. Zarf { code, message, addOnCode } taşır:

codeAnlamı
LICENSE_REQUIREDÜrün lisans ister; sepette de lisans yok
ADDON_ALREADY_OWNEDBu ürün (ya da lisans) zaten aktif
ADDON_ALREADY_GRANTEDÜrünün verdiği her şey zaten yetki setinde var
ADDON_REQUIRES_DEPENDENCYBağımlı olduğu ürün ne sahiplikte ne sepette
ADDON_LIMIT_REDUNDANTEklediği kapasite zaten sınırsız
ADDON_MAX_QUANTITYKatalog tavanı aşılıyor (ör. extra_branch için 100)

Kapı sepet farkındadır: bir ön koşul, sahip olunan bir üründen değil aynı sepetteki kardeş satırdan da karşılanabilir. İlk sepet zaten zorunlu olarak hem lisansı hem ilk modülü içerir; aksi hâlde ilk alışveriş asla geçemezdi. Üretilmiş bir yenileme sepetinde ise sahiplik kontrolleri kapatılır — yenileme, zaten sahip olunanı yeniden ödemektir.

Diğer tahsilat öncesi retler:

  • 409 HARDWARE_OUT_OF_STOCK — donanım satırında yeterli gerçek stok yok (aynı ürünün birden çok satırı toplanarak kontrol edilir).
  • 400 — sepet toplamı 0; PayTR sıfır tutarlı sepeti reddeder, ücretsiz provizyon süperadmin comp yolundan yapılır.

İdempotensi

PayTR retry’leri agresiftir (gövde “OK”/“FAIL” değilse 200 alsa bile tekrar dener). Çift provizyona karşı üç katman vardır:

CheckoutIntent.status kontrolü

Intent zaten provisioned/failed ise hiçbir şeye dokunulmaz.

confirmAndProvision bağımsız idempotenttir

(tenantId, paymentRef) üzerinde idempotenttir ve provizyondan önce o çift için yerleşmiş bir CheckoutIntent arar; uydurma bir paymentRef ile /checkout/confirm çağırmak hiçbir şey provizyonlamaz.

purchase() mevcut satırı döner

Aynı paymentRef için mevcut TenantAddOn satırını, AddOnPurchased olayını yeniden yazmadan döner.

Faturalar

YöntemYolNot
GET/api/v1/me/invoicesÀ-la-carte tenant_invoices kayıtları — bugün kesilen faturalar
GET/api/subscriptions/tenant/invoicesEski abonelik faturaları arşivi (sayfalı: page, pageSize)
GET/api/subscriptions/:id/invoicesTek bir eski aboneliğin faturaları
GET/api/invoices/:invoiceNumberTek eski fatura; /download PDF döner

İki tablo bilerek bir arada yaşıyor: eski invoices tablosu, VUK’un yıllarca saklanmasını zorunlu kıldığı vergi kayıtlarını tutuyor ve NOT NULL subscriptionId taşıyor. Yeni tenant_invoices yazıcısı aynı numara sayacını kullanır (invoice_counters) — iki bağımsız sayaç tek numara formatı üzerinde er ya da geç çakışırdı, üstelik kart çekildikten sonra.

Yenileme (manuel)

Kayıtlı kart ve otomatik tahsilat yoktur. Yenileme, aynı checkout rayından elle ödenir.

AdımZamanlamaNe olur
Döngü üretimiYıl dönümünden 30 gün önce, günlük 06:00 cronSahip olunan her satır için bir kalem taşıyan RenewalCycle dondurulur; fiyatlar üretim anında katalogdan canlı okunup sabitlenir
HatırlatmalarGünlük 09:00 cronKalan 30 / 7 / 1 günde bir kez; gönderim remindersSent dizisine yazılır, aynı hatırlatma iki kez gitmez
Ödemesiz dönem (grace)Yıl dönümü + 7 günÖdenmemiş satırlar past_due olur ama grant vermeye devam eder
Süre dolumuGünlük 00:30 cronGrace bitince ödenmeyen satırlar expired olur; erişim kararır, veri silinmez

Yenileme sepeti tam liste fiyatındandır: yıl dönümü anına quote edildiği için kalan gün = döngü günü olur ve orantılama tam fiyatı döndürür. Ödenince aynı TenantAddOn satırı yerinde yeniden etkinleşir — yeni satır açılmaz, sahiplik kimliği ve varsa planlanmış kapasite düşüşü (pendingQuantity) korunur.

⚠️

Lisans satırı ödenmezse yalnızca o ürün değil, requiresLicense: true olan her ürünün grant’ları karartılır. Sahiplik satırları ve iş verisi yerinde kalır; lisans ödenince bir sonraki projeksiyonda hepsi geri yanar. Bkz. Yetki Matrisi.

İptal

Ürün iptali marketplace rayındadır ve yalnız ADMIN’e açıktır: DELETE /api/v1/marketplace/addons/:id (varsayılan dönem sonu, ?immediate=true ile anında). Ayrıntılar için Marketplace API.

Kaldırılan raylar

Aşağıdaki uçlar v3.3.0’da kaldırıldı; eski entegrasyonlar bunları çağırıyorsa 404 alır:

KaldırılanYerine
POST /api/subscriptions/:id/change-planYok — geçilecek tier yok; ürün tek tek alınır/bırakılır
POST /api/payments/create-intentPOST /api/v1/checkout/intent
GET/POST /api/payments/bank-transfer/*Yok — tahsilat PayTR rayındadır
POST /api/v1/marketplace/addons/purchasePOST /api/v1/checkout/intent (comp’lar yalnız süperadmin yüzeyinde)
GET /api/subscriptions/usage/snapshotGET /api/v1/me/licensing
GET /api/subscriptions/effective-features (hâlâ mount’lu ama plan tablosuna bağlı: currentPlan yoksa 404)GET /api/v1/entitlements/me
TRIAL_ENDED kilidi / PLAN_SELECTION_REQUIRED zarfıYok — çekirdek ücretsiz olduğu için global abonelik durumu kapısı kaldırıldı; kapılar rota bazında ENTITLEMENT_REQUIRED döner

İlgili

  • Yetki Matrisi — ücretsiz taban, ürün grant’ları, katlama kuralları ve ENTITLEMENT_REQUIRED zarfı.
  • Marketplace API — katalog listeleme, sahip olunanlar ve iptal.
  • API Temelleri — taban URL, auth realm’leri, şube kapsamı, hata zarfı, rate-limit katmanları.
  • Webhook’lar — bu raylar onaylandığında tetiklenen ödeme ve checkout olayları.