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:
| Yetenek | Gerekli rol |
|---|---|
| Katalog fiyat listesini okuma | Public (@Public, kimliksiz) |
| Lisans durumunu / faturaları okuma | Kimliği doğrulanmış herhangi bir kullanıcı |
Sepeti fiyatlandırma (/checkout/quote) | Kimliği doğrulanmış herhangi bir kullanıcı |
| Checkout intent / confirm | ADMIN, MANAGER |
| Ürün iptali | Yalnı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öntem | Yol | Realm / rol | Amaç |
|---|---|---|---|
GET | /api/v1/catalog/pricing | Public | Yayınlanmış katalog + fiyatlar (?locale=tr) |
GET | /api/v1/me/licensing | Personel JWT | Lisans durumu, sahip olunanlar, kontörler, yenileme, teklifler, satın alınabilirlik |
GET | /api/v1/me/invoices | Personel JWT | Bu tenant’ın kalem kalem à-la-carte faturaları |
GET | /api/v1/entitlements/me | Personel JWT | Katlanmış yetki seti |
POST | /api/v1/checkout/quote | Personel JWT | Karma sepeti fiyatlandır (yazma yok) |
POST | /api/v1/checkout/start | ADMIN, MANAGER | Yönlendirme öncesi teklifi sabitle (yeniden fiyatlandırma) |
POST | /api/v1/checkout/intent | ADMIN, MANAGER | PayTR iframe token’ı + paymentRef üret |
POST | /api/v1/checkout/confirm | ADMIN, MANAGER | Yerleşmiş bir intent’i provizyona çevir (idempotent) |
POST | /api/webhooks/paytr | PayTR (HMAC + IP allowlist) | Ödeme sonucu geri-çağrısı |
DELETE | /api/v1/marketplace/addons/:id | ADMIN | Sahip olunan bir ürünü iptal et |
GET | /api/subscriptions/plans | Public | Kalıcı olarak boş dizi (aşağıya bakın) |
GET | /api/subscriptions/tenant/invoices | ADMIN, MANAGER | Eski abonelik faturaları arşivi (sayfalı) |
GET | /api/invoices/:invoiceNumber | ADMIN, MANAGER | Tek 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:
| Alan | Açıklama |
|---|---|
code | Değişmez ürün kodu (ör. module_inventory) |
name / description | locale için yerelleştirilmiş metin (yoksa TR fallback) |
kind | license | module | integration | capacity | credit | service |
billing | annual | oneTime |
priceCents | KDV dahil kuruş — tam yıllık liste fiyatı |
currency | Her zaman TRY |
creditKind / creditUnits | Yalnızca kontör paketlerinde |
requiresLicense | Kullanmak için canlı lisans gerekip gerekmediği |
sortOrder | Vitrin 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 |
|---|---|
entitlements | features, limits, integrations, computedAt |
license | status, anchorAt, anniversaryAt, daysRemaining |
credits | Kontör türü → kalan bakiye |
owned | Sahip olunan her satır: code, kind, quantity, pendingQuantity, status, periodEnd, chargedCents, renewalCents, origin |
renewal | Açık yenileme döngüsü: cycleId, anniversaryAt, graceEndsAt, totalCents, daysLeft |
offers | Her 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ğer | Anlamı |
|---|---|
none | Hiç lisans alınmamış |
active | Lisans canlı |
grace | Lisans satırı past_due — ödemesiz dönem sürüyor |
expired | Yı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.
| Mod | Ne zaman | Fiyat |
|---|---|---|
full | Kalan gün = döngü günü (ilk alım veya tam yıl dönümünde alım) | Tam liste fiyatı |
prorated | Yıl dönümüne 14 günden fazla var | liste × kalanGün / döngüGünü |
rollForward | Yı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 === subtotalCentsdaima 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ı - netCentsTRY 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>"]
}'| Alan | Zorunlu | Not |
|---|---|---|
cart | Evet | En az 1 kalem |
buyer | Evet | PayTR’ın gördüğü ve fraud skorlamasına giren bilgiler; telefon E.164’e normalize edilir |
acceptedDocumentIds | Evet | Tam 3 adet: KVKK, Mesafeli Satış, İade Politikası. /legal/documents/:kind/current ucundan alınır |
returnUrl | Hayır | Mutlak http(s) URL olmalı (açık yönlendirme koruması) |
branchId | Hayır | Donanım gönderimi için tenant’a ait aktif şube |
referralCode | Hayır | Intent 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:
code | Anlamı |
|---|---|
LICENSE_REQUIRED | Ürün lisans ister; sepette de lisans yok |
ADDON_ALREADY_OWNED | Bu ürün (ya da lisans) zaten aktif |
ADDON_ALREADY_GRANTED | Ürünün verdiği her şey zaten yetki setinde var |
ADDON_REQUIRES_DEPENDENCY | Bağımlı olduğu ürün ne sahiplikte ne sepette |
ADDON_LIMIT_REDUNDANT | Eklediği kapasite zaten sınırsız |
ADDON_MAX_QUANTITY | Katalog 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:
409HARDWARE_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öntem | Yol | Not |
|---|---|---|
GET | /api/v1/me/invoices | À-la-carte tenant_invoices kayıtları — bugün kesilen faturalar |
GET | /api/subscriptions/tenant/invoices | Eski abonelik faturaları arşivi (sayfalı: page, pageSize) |
GET | /api/subscriptions/:id/invoices | Tek bir eski aboneliğin faturaları |
GET | /api/invoices/:invoiceNumber | Tek 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ım | Zamanlama | Ne olur |
|---|---|---|
| Döngü üretimi | Yıl dönümünden 30 gün önce, günlük 06:00 cron | Sahip olunan her satır için bir kalem taşıyan RenewalCycle dondurulur; fiyatlar üretim anında katalogdan canlı okunup sabitlenir |
| Hatırlatmalar | Günlük 09:00 cron | Kalan 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 dolumu | Günlük 00:30 cron | Grace 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ılan | Yerine |
|---|---|
POST /api/subscriptions/:id/change-plan | Yok — geçilecek tier yok; ürün tek tek alınır/bırakılır |
POST /api/payments/create-intent | POST /api/v1/checkout/intent |
GET/POST /api/payments/bank-transfer/* | Yok — tahsilat PayTR rayındadır |
POST /api/v1/marketplace/addons/purchase | POST /api/v1/checkout/intent (comp’lar yalnız süperadmin yüzeyinde) |
GET /api/subscriptions/usage/snapshot | GET /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_REQUIREDzarfı. - 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ı.