Kimlik Doğrulama
Cüzdan API'si ayrı yüzeylerde çalışır ve her yüzeyin kendi kimlik modeli vardır. Yanlış yüzeyi seçmek en sık yapılan entegrasyon hatasıdır: sunucunuzun çağıracağı uçlar /api/v1/wallet/*, son kullanıcının uygulamasının çağıracağı uçlar /api/portal/v1/wallet/*, operasyon ekibinizin kullandığı uçlar /api/panel/v1/wallet/* altındadır. Üç yüzeyin kimlik modeli şöyledir; ölçülmüş uç sayıları ve yüzeyler arası örtüşme için bir alttaki matrise bakın.
| Yüzey | Model | Kim taşır |
|---|---|---|
| Dış API (s2s) | Ed25519 imzalı istek (X-Key-Id + X-Date + X-Nonce + Digest + X-Signature) | Yalnız sizin backend'iniz |
| Portal / SDK | Publishable key (pk_live_…) → müşteri oturumu (JWT) → hassas işlemde step-up | Son kullanıcının cihazı |
| Merchant panel | Panel oturumu + RBAC izin kodu + dört-göz | Operasyon ekibiniz |
Base URL: https://api-gateway.lunixi.com.
Yüzey karşılaştırma matrisi
Aslında dört yüzey vardır: üçü yukarıdaki kimlik modellerinden birine bağlıdır, dördüncüsü terminal aktivasyonuna ayrılmış tek uçtur. Aşağıdaki uç sayıları dokümantasyondan değil, çalışan controller'lardan sayılmıştır.
| Yüzey | Rota kökü | Kimlik | Uç |
|---|---|---|---|
| Merchant panel | /api/panel/v1/wallet/** | Panel oturumu (JWT) + RBAC izin kodu; para-kritik işlemde dört-göz | 186 |
| Dış API (s2s) | /api/v1/wallet/** | Ed25519 istek imzası; ±5 dk pencere, tek kullanımlık nonce, anahtar başına IP allow-list | 60 |
| Portal / SDK | /api/portal/v1/wallet/** | Publishable key → OTP → son kullanıcı oturumu (merchant değil) | 87 |
| Terminal aktivasyonu | /api/v1/wallet/terminals/activate | ⚠️ Kimlik guard'ı yok — yetki kanıtı tek kullanımlık aktivasyon kodudur | 1 |
Panel'in 186 ucunun 162'si ana controller'da, 10'u dört-göz talep controller'ında, kalan 14'ü kart (7), nakit çekim (4) ve kart olay webhook'u (3) controller'larındadır.
Panel ile s2s ne kadar örtüşüyor?
Kendi panelinizi s2s API ile yazamazsınız. Panel'in ana ve dört-göz talep controller'larındaki 172 uçtan yalnız 33'ünün s2s karşılığı vardır. Kalan 139 uç panele özeldir: imza anahtarı bu kapıyı açmaz, çünkü panel uçları BearerAuthGuard (JWT) + PermissionsGuard ardındadır ve imzalı istek bu iki guard'ın hiçbirini geçmez.
| Kesişim | Uç | Ne var |
|---|---|---|
| Yalnız panel | 139 | Aşağıdaki tabloya bakın |
| Panel + s2s ortak | 33 | Son kullanıcı/kurumsal hesap okuma-yazma, operasyon okuma, toplu ödeme akışı, QR üretme/iptal, fees/quote, KYC seviyesi okuma |
| Yalnız s2s | 25 | Paranın kendisi: transfers/w2w, transfers/w2iban, withdrawals/bank, payments/buy, collect, payments/:id/approve, topups/card, agent-topups, qr/parse, qr/:id/image, wallets/lookup … |
Panelde olup s2s API'de karşılığı olmayan 139 yetenek:
| Kategori | Uç | Örnek |
|---|---|---|
| Program yaşam döngüsü + politikalar | 32 | programs, programs/:id/sponsor-policy, bank-rails, provider-switches, spend-policies, otp-policy, shared-limit-caps |
| Kampanya / sadakat / segment | 29 | campaigns/**, segments/**, loyalty/** |
| Terminal + QR üretici kodu | 18 | terminals/** (kurulum/provisioning, kimlik bilgisi döndürme/iptal, askıya alma, toplu işlem), qr/producer-codes/** |
| Ücret / komisyon / limit şemaları | 17 | fee-schedules/**, commission-schedules/**, limit-schemes/** ve sürüm submit/approve zinciri |
| Raporlar | 11 | reports/overview, reports/operations, reports/fees, reports/campaigns, reports/custody, reports/terminals, operations/export, toplu ödeme ekstreleri |
| Dört-göz müdahale / operasyon | 16 | operations/:id/intervention-requests, withdrawals/:id/intervention-requests, cash-out/:id/hold-requests, suspense-funds/:id/match-requests, .../return, topups/:id/refund-requests, end-users/:id/kyc-level/override-requests, intervention-requests/**, ops-health, bill-payments |
| Son kullanıcı / kurumsal (panel-özel) | 7 | end-users/:id/close, .../unfreeze, .../wallets, .../kyc-level/history, .../merchant-category, corporate-accounts/:id |
| Fatura / kripto | 5 | invoices/**, crypto/balances |
| Entegrasyon / sağlayıcı / agents | 3 | integrations/catalog, agents |
| Toplu ödeme (panel-özel) | 1 | bulk-payouts/:id/cancel |
Bu sayılar apps/api-gateway/src/wallet/*.controller.ts dosyalarındaki rota dekoratörlerinden sayılmıştır. Uç eklendikçe değişir; oran değil, yön kalıcıdır: panel yüzeyi s2s'ten kat kat geniştir ve iki yüzey ayrı controller'lardır.
Dış API (s2s) — imzalı istek
Cüzdanın s2s yüzeyi imza tabanlıdır: her istek Ed25519 özel anahtarınızla imzalanır. Bu yüzeyde ayrıca bir Bearer API key göndermeniz gerekmez — imzanın kendisi kimliğinizdir.
Anahtarınızı alın
Merchant panelde API Anahtarları bölümünden bir secret key oluşturun. İki kurulum (provisioning) modeli vardır:
- Platform (reveal-once) — anahtar çiftini platform üretir, açık anahtarı kaydeder, özel anahtarı size yalnız bir kez gösterir. O ekranı kapattıktan sonra bir daha erişilemez; kaybederseniz rotate edersiniz.
- BYO (kendi anahtarını getir) — anahtar çiftini siz üretir, yalnız açık anahtarı yüklersiniz. Özel anahtar hiçbir zaman platforma gelmez; en güvenli seçenek budur.
# BYO için Ed25519 anahtar çifti üretimi
openssl genpkey -algorithm ed25519 -out lunixi-signing.key
openssl pkey -in lunixi-signing.key -pubout -out lunixi-signing.pubAnahtarınızın key id'si (kid) X-Key-Id header'ında gider.
Anahtar izinleri
İmza doğrulandıktan sonra her istek iki denetimden daha geçer:
- Ürün — anahtarınızın ürün listesinde cüzdan ürünü (
WALLET_SERVICE) bulunmalıdır. Bulunmazsa okuma uçları dahil her uç403 PRODUCT_NOT_ENABLEDdöner. - İzin — anahtarınız, çağırdığınız uç için gereken cüzdan iznini taşımalıdır. İzinler fail-closed uygulanır: anahtarın izni olmayan bir çağrı
403 WALLET_INSUFFICIENT_SCOPEdöner; hiçbir cüzdan izni olmayan bir anahtar cüzdanda hiçbir işlem yapamaz.
İzinler hiyerarşik değildir: okuma izni yazma yetkisi vermez, kurulum izni hiçbir zaman para hareket ettirmez.
| Uçlar | Anahtarda gereken izin |
|---|---|
Okumalar: GET programs/me, son kullanıcı, bakiye, limit ve KYC seviyesi okumaları, operasyonlar ve dekontlar, yükleme / ödeme / çekim detayı, işletme komisyonu, QR listesi / detayı / görseli, POST qr/parse, toplu ödeme listesi / detayı / kalemleri ve quote uçları (fees/quote, transfers/w2w/quote, transfers/w2iban/quote, withdrawals/bank/quote) | wallet:program:read, wallet:enduser:read, wallet:operation:read, wallet:qr:read izinlerinden herhangi biri |
Kurulum: GET ping, PUT programs/:programId/collection-anchor, POST end-users, POST end-users/:id/freeze, POST end-users/:id/wallets, POST wallets/:id/accounts, POST wallets/lookup, havale talimatları | wallet:program:manage veya wallet:enduser:manage |
PUT end-users/:id/kyc-level | wallet:enduser:kyc-override |
Cüzdan içi para hareketi: transfers/w2w başlatma ve tamamlama, payments/buy, payments/:id/approve, collect, topups/card, agent-topups, topups/:id/refund | wallet:operation:intervene |
Platform dışına para çıkışı: transfers/w2iban ve withdrawals/bank başlatma ve tamamlama; test ortamındaki gelen havale simülasyonu (bank-integrations/inbound-credits) | wallet:bank-payout:operate — açık devre (Growth) planında sunulur |
POST qr, POST qr/:id/revoke | wallet:qr:manage |
POST bulk-payouts, POST bulk-payouts/:id/reject; kurumsal hesaplar (POST / GET corporate-accounts, employees/import, employees/offboard) | wallet:bulk:create |
POST bulk-payouts/:id/submit, /approve, /process, /retry-failed | wallet:bulk:approve |
Toplu ödemede dört-göz: bir partiyi, onu oluşturan anahtardan farklı bir anahtarla onaylamanız gerekir; oluşturan anahtarın onayı WALLET_FOUR_EYES_VIOLATION ile reddedilir. Partiyi oluşturup onaya gönderen hazırlayıcı anahtar hem wallet:bulk:create hem wallet:bulk:approve iznini taşır; partiyi onaylayan kontrolcü anahtar ise wallet:bulk:approve iznini taşıyan ayrı bir anahtardır.
Pratik sonuçları:
- Raporlama ya da destek entegrasyonunuza yalnız okuma izinleri verebilirsiniz; bu anahtar
transfers/w2wya dawithdrawals/bankçağıramaz. - Platform dışına para çıkışı ayrı bir izin ister:
wallet:operation:intervenetek başına IBAN'a ya da banka hesabına para gönderemez. - Bir çağrı
WALLET_INSUFFICIENT_SCOPEdönerse yukarıdaki tablodaki izni panelden anahtarınıza ekleyin. - Üretim anahtarlarınızda IP allow-list'i de mutlaka doldurun.
İstek header'ları
| Header | Değer | Zorunlu |
|---|---|---|
X-Key-Id | Anahtarınızın key id'si | Evet |
X-Date | ISO 8601 zaman damgası — sunucu saatine göre ±5 dakika | Evet |
X-Nonce | İstek başına benzersiz değer (UUID) — tek kullanımlık | Evet |
Digest | Body'nin SHA-256=<base64> hash'i — body boşluksuz JSON olarak gönderilir, bkz. Digest | Body varsa |
X-Signature | Kanonik string üzerine Base64 Ed25519 imzası | Evet |
Idempotency-Key | Çift işlem koruması, 1–128 karakter | Idempotency-Key bölümünde listelenen uçlarda |
Kanonik string
İmzalanan metin, satırları \n ile birleştirilmiş şu yapıdır:
POST
/api/v1/wallet/transfers/w2w
X-Date:2026-08-07T12:00:00.000Z
X-Nonce:9f8d2c4e-1a3b-4c5d-8e7f-0a1b2c3d4e5f
Digest:SHA-256=k6I5cUt...=İkinci satır tam istek path'idir ve varsa query string'i de içerir. GET /api/v1/wallet/operations?limit=20 imzalarken ikinci satır /api/v1/wallet/operations?limit=20 olmalıdır — yalnız path'i imzalarsanız INVALID_SIGNATURE alırsınız. Body yoksa Digest satırı hiç yazılmaz.
Digest ve boşluksuz JSON
Sunucu JSON body'yi ayrıştırır ve SHA-256 özetini body'nin boşluksuz biçimi üzerinden yeniden hesaplar — satır sonu yoktur, öğeler arasında boşluk yoktur. Body'yi boşluksuz JSON olarak gönderin ve Digest değerini tam olarak gönderdiğiniz metin üzerinden hesaplayın. Girintili (pretty-print) bir body, içeriği aynı olsa bile 401 INVALID_DIGEST alır. Node.js'te girinti argümanı vermeden çağrılan JSON.stringify(body) doğru biçimi üretir.
Idempotency-Key
Header'ın başındaki ve sonundaki boşluklar kırpılır; değer 1–128 karakter olmalıdır. Aşağıdaki uçlarda zorunludur; header eksik ya da boşsa istek 400 ile reddedilir:
- Son kullanıcı ve cüzdan:
POST end-users,POST end-users/:id/freeze,PUT end-users/:id/kyc-level,POST end-users/:id/wallets,POST wallets/:id/accounts - Kurumsal hesaplar:
POST corporate-accounts,employees/import,employees/offboard - Transfer ve çekim:
transfers/w2w,transfers/w2ibanvewithdrawals/bank— hem başlatma hem tamamlama - Yükleme ve ödeme:
topups/card,agent-topups,payments/buy,payments/:id/approve,collect POST qr,POST bulk-payouts
Quote uçlarında, GET uçlarında, PUT programs/:programId/collection-anchor, havale talimatları, bank-integrations/inbound-credits, topups/:id/refund, qr/:id/revoke, qr/parse, wallets/lookup uçlarında ve toplu ödemenin submit, approve, reject, process, retry-failed çağrılarında zorunlu değildir.
Çalışan örnek
import { createHash, createPrivateKey, sign, randomUUID } from 'node:crypto';
const BASE = 'https://api-gateway.lunixi.com';
const path = '/api/v1/wallet/transfers/w2w';
const body = JSON.stringify({
endUserId: '8d4c2a1f-3b6e-4f7a-9c5d-2e8b1a0f6c34',
sourceWalletAccountId: 'c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47',
destinationPhone: '+905321234567',
amount: '10050', // minor-unit string — ₺100,50
currency: 'TRY',
paymentPurpose: 'P2P',
});
const digest = 'SHA-256=' + createHash('sha256').update(body).digest('base64');
const date = new Date().toISOString();
const nonce = randomUUID();
const canonical = ['POST', path, `X-Date:${date}`, `X-Nonce:${nonce}`, `Digest:${digest}`].join('\n');
const signature = sign(
null, // Ed25519 — ayrı digest algoritması yok
Buffer.from(canonical),
createPrivateKey(process.env.LUNIXI_SIGNING_KEY_PEM),
).toString('base64');
const res = await fetch(BASE + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Key-Id': process.env.LUNIXI_KEY_ID,
'X-Date': date,
'X-Nonce': nonce,
'Digest': digest,
'X-Signature': signature,
'Idempotency-Key': 'p2p-9988-attempt-1',
},
body,
});Bağlantıyı doğrulayın
İmzalamayı ilk kez kurarken body'siz ve idempotency gerektirmeyen bir uçla test edin:
GET /api/v1/wallet/pingping, anahtarda bir kurulum izni (wallet:program:manage veya wallet:enduser:manage) gerektirir; yalnız okuma izni olan bir anahtar 403 WALLET_INSUFFICIENT_SCOPE alır. 200 dönüyorsa anahtar, saat ve kanonik string'iniz doğrudur. ping çalışmadan para hareketi uçlarını denemeyin — hata ayıklaması çok daha zordur.
Yanıt biçimi
Başarılı yanıtlar gateway zarfıyla döner; her sayfada belgelenen alanlar data altındadır:
{
"status": "success",
"code": "SUCCESS",
"message": "…",
"data": {
"operationId": "7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93",
"status": "CHALLENGED"
},
"requestId": "…"
}- Listeler
data.itemsvedata.nextCursordöner; sonraki sayfa içinnextCursordeğerinicursorolarak geri gönderin. Operasyon listesi kayıtlarınıdata.operationsaltında döner. - Hatalar HTTP durum koduyla ve
code(örneğinWALLET_NOT_FOUND) ilemessagealanlarını taşıyan bir body ile döner. - Tüm kimlikler UUID'dir. Tutarlar en fazla 18 haneli, minor-unit rakam dizgesidir:
"12550"₺125,50 demektir.
Hata kodları
| Kod | HTTP | Anlamı | Ne yapmalı |
|---|---|---|---|
AUTH_HEADERS_MISSING | 401 | Zorunlu header eksik | X-Key-Id, X-Date, X-Nonce, X-Signature gönderin |
INVALID_DATE | 401 | X-Date 5 dakikalık pencerenin dışında | Sunucu saatinizi NTP ile senkronize edin |
NONCE_REPLAY | 401 | Aynı nonce daha önce kullanıldı | Her istekte yeni nonce üretin |
INVALID_KID | 401 | Key id bulunamadı veya aktif değil | Panelden anahtarın durumunu kontrol edin |
MERCHANT_SUSPENDED | 403 | Hesap aktif değil | Destek ile görüşün |
IP_NOT_ALLOWED | 403 | İstek IP'si allow-list'te değil | Panelden sunucu IP'nizi ekleyin |
DIGEST_MISSING | 401 | Body var ama Digest yok | Body'nin SHA-256 hash'ini ekleyin |
INVALID_DIGEST | 401 | Digest gövdeyle uyuşmuyor | Body'yi boşluksuz JSON olarak gönderin ve digest'i tam olarak o metin üzerinden hesaplayın |
INVALID_SIGNATURE | 401 | İmza doğrulanamadı | Kanonik stringi kontrol edin (query string, Digest satırının varlığı) |
PRODUCT_NOT_ENABLED | 403 | Anahtarın ürün listesinde cüzdan ürünü yok | Anahtarın ürünlerine WALLET_SERVICE ekleyin |
WALLET_INSUFFICIENT_SCOPE | 403 | Anahtarda bu çağrı için gereken cüzdan izni yok | Anahtar izinleri tablosundaki izni verin |
Nonce kayıtları ~6 dakika saklanır; X-Date penceresi ±5 dakikadır. Yani "eski bir isteği tekrar oynatma" saldırısı her iki kapıdan da geçemez.
Portal / SDK — publishable key + müşteri oturumu
Son kullanıcının uygulaması imza anahtarı taşımaz — taşısaydı, uygulamayı kuran herkes tenant'ınızın tüm cüzdanlarına erişebilirdi.
Publishable key ile tanıtın
Panelden bir publishable key (pk_live_…) oluşturun ve uygulamanızın kaynağını (allowedOrigins) girin. Bu anahtar tarayıcıya/uygulamaya konabilir; tek başına hiçbir veriye erişmez.
allowedOrigins zorunludur ve joker (*) kabul edilmez. Anahtar yalnız listelediğiniz origin'lerden çalışır.
Kullanıcıyı oturum açtırın
POST /api/portal/v1/wallet/auth/register veya /login çağrılır; telefon numarasına OTP gider. POST /api/portal/v1/wallet/auth/otp/verify ile doğrulanınca müşteri oturumu (access + refresh token) açılır.
Oturum token'ı ile çağırın
Bundan sonraki tüm /api/portal/v1/wallet/me/* çağrıları oturum token'ını taşır ve yalnızca o kullanıcının kendi verisine erişir.
Hassas işlemde step-up
Transfer, çekim, QR ödeme gibi para çıkaran işlemlerde oturum tek başına yetmez: OTP kodu (otpCode) ya da güvenilir aksiyon token'ı (trustedActionToken) istenir.
Ayrıntı: Portal — Kimlik & Oturum.
Merchant panel
Panel uçları (/api/panel/v1/wallet/*) panel oturumunuzla çalışır ve her uç bir RBAC izin koduna bağlıdır (wallet:program:manage, wallet:operation:intervene, wallet:bulk:approve …). Para-kritik aksiyonlarda ek olarak dört-göz kuralı vardır.
Dört-göz kuralı rol kapısıyla değil, kimlik karşılaştırmasıyla zorlanır: isteği yapan ile onaylayan aynı kişi olamaz. OWNER/ADMIN rolleri her iki izni de taşısa bile aynı kullanıcı kendi isteğini onaylayamaz.
Ayrıntı: Panel — Genel Bakış & Yetkiler.
Terminal aktivasyonu — kimlik guard'ı olmayan tek uç
POST /api/v1/wallet/terminals/activate, cüzdan yüzeyinde kimlik guard'ı taşımayan tek uçtur: ne imza, ne panel oturumu, ne müşteri oturumu ister.
Bu bir eksik değil, tavuk-yumurta probleminin çözümüdür: henüz aktive olmamış bir cihazın kimliği yoktur — imzalayacağı bir anahtarı da yoktur, çünkü anahtarı bu uç kazandıracaktır.
Yetki kanıtı, cihazın gönderdiği tek kullanımlık aktivasyon kodudur:
| Koruma | Nasıl |
|---|---|
| Tahmin edilemezlik | 70 bit entropi (14 karakter × 32 harfli alfabe) |
| Tek kullanımlık | Atomik olarak yakılır ve geri bırakılmaz — kurulum başarısız olsa bile |
| Kısa ömür | Varsayılan 15 dakika |
| Kaba kuvvet | IP başına iki pencereli hız sınırı: 60 sn'de 5, 1 saatte 30 deneme |
| Tenant sızması | Tenant beyan edilemez; koddan türetilir |
Bu uç hiçbir özel anahtar kabul etmez. Kendi anahtarını getiren form faktörlerinde (ATTENDED_POS, MOBILE_APP) cihaz yalnız açık anahtarı yükler; platform form faktörlerinde (SERVER_ECR, WEB_TILL) açık anahtar gönderilemez ve platformun ürettiği özel PEM yanıtta bir kez döner, hiçbir yerde saklanmaz.
Terminalin kendisini oluşturmak, aktivasyon kodu üretmek, kimlik bilgisini döndürmek/iptal etmek ise panel yüzeyindedir (wallet:terminal:manage) — bkz. Panel — QR & Terminaller.
Anahtar yönetimi
- Rotasyon — panelden rotate ettiğinizde yeni anahtar üretilir ve eskisi iptal edilir. Kesintisiz geçiş için önce yeni anahtarı devreye alın, trafiği taşıyın, sonra eskisini iptal edin.
- İptal — sızıntı şüphesinde anahtarı derhal iptal edin; iptal edilen
kidile gelen isteklerINVALID_KIDalır. - Özel anahtar asla log'a, repository'ye, mobil uygulamaya veya tarayıcıya girmemelidir. Platform da özel anahtarınızı saklamaz.