Pricing & Portal SDK
Abonelikleri kendi web sitenizden satın. İki yapı taşı bunu mümkün kılar:
- Public Catalog API — plan, ürün ve marka bilgilerinizi publishable key ile sunan, tarayıcıdan güvenle çağrılabilen salt-okunur bir API. Pricing tablonuzu canlı veriden render edin; fiyatı tek yerden değiştirin, siteniz otomatik takip etsin.
- Portal JS SDK —
cdn.lunixi.comüzerinden tek script; ziyaretçiyi "plan seç"ten "kart kaydedildi, abonelik aktif"e taşır. Hazır portal UI'ı kullanın, kendi komponentlerinizle tamamen headless çalışın ya da ikisini karıştırın.
Harici checkout'a yönlendirme yok. Kart verisi SDK'nın güvenli formunda toplanır ve sunucunuza asla uğramaz.
Tutarlar minor-unit (kuruş) integer'dır: "price": 49900 = ₺499,00.
Parçalar nasıl birleşir
- Kayıt öncesi ziyaretçinin tek ihtiyacı fiyatlardır: publishable key ile çalışan catalog katmanı.
- Abonelik anında ziyaretçi müşteriye dönüşür: backend'iniz o müşteri için bir SDK oturumu açar ve işi tarayıcıdaki SDK bitirir — ya da aboneliği backend'iniz Merchant API ile kendisi oluşturur.
Katman 1 — Public Catalog API
Doğrudan tarayıcıdan çağrılmak üzere tasarlanmış salt-okunur endpoint'ler:
| Method | Path | Döndürdüğü |
|---|---|---|
| GET | /api/public/v1/subscription/plans | Aktif planlar (fiyat, para birimi, deneme, faturalama ritmi, vitrin özellikleri) |
| GET | /api/public/v1/subscription/products | Aktif ürünler |
| GET | /api/public/v1/subscription/branding | Logo, marka renkleri, kullanım koşulları / gizlilik URL'leri |
Kimlik doğrulama: publishable key + Origin allow-list
Catalog çağrıları Authorization header'ında publishable key'inizle (mk_live_pk_…) doğrulanır. Client-side koda gömülmesi güvenlidir — yalnızca public kataloğu okuyabilir; asla para hareketi yapamaz, müşteri verisi açamaz.
Tüm yüzeyi iki anahtar korur; ikisi de merchant panelinden yönetilir (Portal → SDK ayarları):
- Origin allow-list — API'yi çağıracak her origin'i ekleyin (örn.
https://www.example.com). Listede olmayan origin'lerden gelen istekler reddedilir. - SDK enabled — gömülü yüzeyin ana şalteri.
Publishable key tasarım gereği herkese açıktır; başka sitelerin kataloğunuzu kendi sitesiymiş gibi kullanmasını engelleyen şey Origin allow-list'tir. Listeyi dar tutun — yalnızca pricing sayfanızı sunduğunuz origin'leri ekleyin. Ed25519 özel anahtarınız ve ondan bastığınız erişim token'ı (at_live_…) tarayıcıda asla görünmemelidir.
İstek
curl "https://api-gateway.lunixi.com/api/public/v1/subscription/plans?locale=tr" \
-H "Authorization: Bearer mk_live_pk_xxxxxxxxxxxx" \
-H "Origin: https://www.example.com"locale parametresi katalog çevirilerini döndürür — locale=tr gönderin, tanımladığınız yerlerde yerelleştirilmiş ad ve açıklamalar gelsin.
Yanıt
Planlar data dizisinde döner; cursor pagination için nextPageToken gelir:
{
"success": true,
"data": [
{
"id": "pln_9f3a1c",
"productId": "prd_4b2e88",
"code": "pro-monthly",
"name": "Pro Aylık",
"description": "Büyüyen ekipler için",
"price": 49900,
"currency": "TRY",
"trialPeriodDays": 7,
"billing": { "frequency": "MONTHLY", "interval": 1 }
}
],
"nextPageToken": ""
}Katman 2 — Portal JS SDK
Tek script, dört mod:
<script src="https://cdn.lunixi.com/lunixi-portal.min.js"></script>| Mod | Giriş noktası | Ne verir | Oturum |
|---|---|---|---|
| 0 — Catalog | Lunixi.portal.catalog(cfg) | Pricing veri client'ı (plans / products / branding) | Yok — sadece publishable key |
| 1 — Tam portal | Lunixi.portal.mount(target, cfg) | Sayfanıza render edilen eksiksiz, markalı portal UI | sdkMode: 'FULL' |
| 2 — Headless | Lunixi.portal.create(cfg) | Saf API client — UI'ın %100'ü sizde | sdkMode: 'HEADLESS' |
| 3 — Hibrit | Lunixi.portal.plans.mount(...) / Lunixi.portal.cardForm.mount(...) | Kendi layout'unuz içinde tekil widget'lar (plan seçici, güvenli kart formu) | sdkMode: 'HYBRID' |
Mod 0 — pricing verisi, sıfır backend
const catalog = Lunixi.portal.catalog({
publishableKey: 'mk_live_pk_xxxxxxxxxxxx',
locale: 'tr',
});
const plans = await catalog.plans(); // tüm aktif planlar
const products = await catalog.products(); // tüm aktif ürünler
const branding = await catalog.branding(); // logo, renkler, koşul URL'leriBu, aynı Public Catalog API'sinin sarılmış hâlidir — ziyaretçi henüz hiçbir yerde oturum açmamışken pricing tablosunu render etmek için idealdir.
Oturum bootstrap'ı — oturumu backend'iniz üretir
Mod 1–3 belirli bir müşteri adına çalışır; bu yüzden yalnızca backend'inizin oluşturabileceği bir oturuma ihtiyaç duyar. Tarayıcı secret key'inizi asla görmez; onun yerine kısa ömürlü, origin'e bağlı bir oturum alır.
POST /api/v1/subscriptions/portal/sdk-session — Bearer (at_live_…), yalnızca server-to-server:
curl -X POST https://api-gateway.lunixi.com/api/v1/subscriptions/portal/sdk-session \
-H "Authorization: Bearer at_live_5f3e2b1a..." \
-H "Content-Type: application/json" \
-d '{
"customerId": "<customer-id>",
"origin": "https://www.example.com",
"sdkMode": "HYBRID"
}'Yanıt — { portalToken, sessionProof, proofNonce, proofExpiresAt } — tarayıcıdaki SDK'ya iletilir. İstenen origin allow-list'inizde olmalı; sdkMode da panelde etkinleştirdiğiniz modlardan biri olmalıdır.
En basit bağlantı: SDK'ya bootstrap endpoint'inizin URL'sini verin, oturumu kendisi çeksin:
const portal = Lunixi.portal.create({
sessionBootstrapUrl: '/portal-session', // yukarıdaki backend endpoint'iniz
});
await portal.init(); // oturumu doğrular, branding ve izinleri yüklerMod 1 — hazır portal UI
<div id="portal"></div>
<script>
Lunixi.portal.mount('#portal', {
sessionBootstrapUrl: '/portal-session',
locale: 'tr',
});
</script>Self-servis portalın tamamını render eder — planlar, aktif abonelik, faturalar, kart yönetimi — yapılandırdığınız marka görünümüyle. Tek div, bitti.
Mod 2 — headless
Tüm portal yetenekleri düz async metotlar olarak; her şeyi siz render edersiniz:
const portal = Lunixi.portal.create({ sessionBootstrapUrl: '/portal-session' });
await portal.init();
// Müşteri sahipliğini tek kullanımlık kodla (e-posta/SMS) kanıtlar:
await portal.requestOtp();
await portal.verifyOtp(inputtanGelenKod); // → session JWT client içinde tutulur
const me = await portal.me();
const plans = await portal.plans();
const subs = await portal.subscriptions();
const invoices = await portal.invoices();
// Kayıtlı kart / kart token'ı ile abonelik başlatın:
await portal.startSubscription({
planId: secilenPlan.id,
paymentMethodToken: cardToken,
});Portal oturumları OTP doğrulamalıdır: init() sonrasında müşteri, hesap verisine veya paraya dokunan herhangi bir çağrıdan önce tek kullanımlık kodu onaylar. İptal gibi hassas işlemler ek olarak e-posta onay token'ı ister — bkz. Müşteri Portalı.
Mod 3 — hibrit widget'lar
Sayfa düzeniniz sizde kalsın, yalnızca zor parçaları ödünç alın:
// Kendi layout'unuz içinde hazır plan seçici:
Lunixi.portal.plans.mount('#plans', {
sessionBootstrapUrl: '/portal-session',
onSelect: (plan) => showCheckout(plan),
});
// PCI-güvenli kart formu — kart verisi kodunuza hiç değmez:
Lunixi.portal.cardForm.mount('#card', {
sessionBootstrapUrl: '/portal-session',
onSuccess: (cardToken) => subscribe(cardToken),
});Kart formu asla kendiniz yazmamanız gereken parçadır: PAN'ı SDK'nın güvenli bağlamında toplar ve size yalnızca bir token geri verir.
Uçtan uca: pricing tablosu → plan → aktif abonelik
Pricing tablosunu render edin (Mod 0)
const catalog = Lunixi.portal.catalog({ publishableKey: 'mk_live_pk_xxxxxxxxxxxx' });
const [plans, branding] = await Promise.all([catalog.plans(), catalog.branding()]);
for (const plan of plans) {
renderPlanCard(plan, {
onSubscribe: () => startSignup(plan), // ₺: plan.price / 100 olarak biçimlendirin
});
}Müşteriyi kimliklendirin
Ziyaretçi bir plan seçtiğinde onu kendi kayıt/giriş akışınızdan geçirin. Ardından backend'iniz müşteriyi upsert eder (PUT /api/v1/subscriptions/customers, kendi externalId'nizle) ve bir SDK oturumu üretir (POST /api/v1/subscriptions/portal/sdk-session).
Kartı toplayın (Mod 3 kart formu)
Lunixi.portal.cardForm.mount('#card', {
sessionBootstrapUrl: '/portal-session',
onSuccess: (cardToken) => activate(plan, cardToken),
});Aboneliği başlatın — eşit derecede geçerli iki yol
Müşteri her şeyi tarayıcıda, OTP doğrulamalı portal oturumu içinde tamamlar:
const portal = Lunixi.portal.create({ sessionBootstrapUrl: '/portal-session' });
await portal.init();
await portal.requestOtp();
await portal.verifyOtp(inputtanGelenKod);
const sub = await portal.startSubscription({
planId: plan.id,
paymentMethodToken: cardToken,
});
// sub.status → `subscription.activated` düştüğü anda erişimi açınSon müşteri başındayken ve onay + doğrulamayı uçtan uca platformun yönetmesini istediğinizde en iyi seçenek.
Webhook'larla teyit edin
Erişimi HTTP yanıtına göre değil, subscription.created / subscription.activated event'lerine göre açın. Kullanıcı akışın ortasında sekmeyi kapatsa bile gerçeğin kaynağı webhook'tur.
Hatalar
| Durum | Nerede | Anlamı / ne yapmalı |
|---|---|---|
401 | Catalog | Publishable key eksik veya geçersiz — Authorization: Bearer mk_live_pk_… header'ını kontrol edin. |
403 | Catalog / SDK | Origin allow-list'te değil ya da SDK yüzeyi / istenen sdkMode kapalı — panelin portal ayarlarından düzeltin. |
401 | Portal çağrıları | Oturum süresi doldu veya OTP doğrulanmadı — init() → requestOtp() → verifyOtp() akışını yeniden çalıştırın. |
400 | sdk-session | customerId / origin eksik — ikisi de zorunludur. |
404 | sdk-session | Bilinmeyen customerId — önce müşteriyi upsert edin. |
409 | startSubscription | Idempotent tekrar çakışması — SDK idempotency key'leri işlem başına kapsamlar; güvenle yeniden deneyin ya da kendi idempotencyKey'inizi geçin. |
Webhook'lar
| Event | Ne zaman |
|---|---|
subscription.created | Portal start veya Merchant API ile abonelik oluşturuldu. |
subscription.activated | Abonelik canlıya geçti — erişimi şimdi açın. |
card.added | Kart formu tamamlandı, kart vault'a girdi. |
invoice.created / invoice.paid | İlk faturalama döngüsü çalıştı. |
Sonraki adımlar
- Başlarken — aynı zincir, tamamen server-side
- Planlar — deneme süreleri, billing kuralları, dunning
- Müşteri Portalı — hosted portal linkleri, OTP akışı, izinler
- Ürünler — çok dilli pricing sayfaları için katalog çevirileri