Cüzdan
Kimlik Doğrulama

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üzeyModelKim taşır
Dış API (s2s)Ed25519 imzalı istek (X-Key-Id + X-Date + X-Nonce + Digest + X-Signature)Yalnız sizin backend'iniz
Portal / SDKPublishable key (pk_live_…) → müşteri oturumu (JWT) → hassas işlemde step-upSon kullanıcının cihazı
Merchant panelPanel oturumu + RBAC izin kodu + dört-gözOperasyon 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üzeyRota köküKimlikUç
Merchant panel/api/panel/v1/wallet/**Panel oturumu (JWT) + RBAC izin kodu; para-kritik işlemde dört-göz186
Dış API (s2s)/api/v1/wallet/**Ed25519 istek imzası; ±5 dk pencere, tek kullanımlık nonce, anahtar başına IP allow-list60
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 kodudur1

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şimUçNe var
Yalnız panel139Aşağıdaki tabloya bakın
Panel + s2s ortak33Son kullanıcı/kurumsal hesap okuma-yazma, operasyon okuma, toplu ödeme akışı, QR üretme/iptal, fees/quote, KYC seviyesi okuma
Yalnız s2s25Paranı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:

KategoriUçÖrnek
Program yaşam döngüsü + politikalar32programs, programs/:id/sponsor-policy, bank-rails, provider-switches, spend-policies, otp-policy, shared-limit-caps
Kampanya / sadakat / segment29campaigns/**, segments/**, loyalty/**
Terminal + QR üretici kodu18terminals/** (kurulum/provisioning, kimlik bilgisi döndürme/iptal, askıya alma, toplu işlem), qr/producer-codes/**
Ücret / komisyon / limit şemaları17fee-schedules/**, commission-schedules/**, limit-schemes/** ve sürüm submit/approve zinciri
Raporlar11reports/overview, reports/operations, reports/fees, reports/campaigns, reports/custody, reports/terminals, operations/export, toplu ödeme ekstreleri
Dört-göz müdahale / operasyon16operations/: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)7end-users/:id/close, .../unfreeze, .../wallets, .../kyc-level/history, .../merchant-category, corporate-accounts/:id
Fatura / kripto5invoices/**, crypto/balances
Entegrasyon / sağlayıcı / agents3integrations/catalog, agents
Toplu ödeme (panel-özel)1bulk-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.pub

Anahtarı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:

  1. Ü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_ENABLED döner.
  2. İ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_SCOPE dö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çlarAnahtarda 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-levelwallet: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/refundwallet: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/revokewallet: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-failedwallet: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/w2w ya da withdrawals/bank çağıramaz.
  • Platform dışına para çıkışı ayrı bir izin ister: wallet:operation:intervene tek başına IBAN'a ya da banka hesabına para gönderemez.
  • Bir çağrı WALLET_INSUFFICIENT_SCOPE dönerse yukarıdaki tablodaki izni panelden anahtarınıza ekleyin.
  • Üretim anahtarlarınızda IP allow-list'i de mutlaka doldurun.

İstek header'ları

HeaderDeğerZorunlu
X-Key-IdAnahtarınızın key id'siEvet
X-DateISO 8601 zaman damgası — sunucu saatine göre ±5 dakikaEvet
X-Nonceİstek başına benzersiz değer (UUID) — tek kullanımlıkEvet
DigestBody'nin SHA-256=<base64> hash'i — body boşluksuz JSON olarak gönderilir, bkz. DigestBody varsa
X-SignatureKanonik string üzerine Base64 Ed25519 imzasıEvet
Idempotency-KeyÇift işlem koruması, 1–128 karakterIdempotency-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/w2iban ve withdrawals/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/ping

ping, 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.items ve data.nextCursor döner; sonraki sayfa için nextCursor değerini cursor olarak geri gönderin. Operasyon listesi kayıtlarını data.operations altında döner.
  • Hatalar HTTP durum koduyla ve code (örneğin WALLET_NOT_FOUND) ile message alanları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ı

KodHTTPAnlamıNe yapmalı
AUTH_HEADERS_MISSING401Zorunlu header eksikX-Key-Id, X-Date, X-Nonce, X-Signature gönderin
INVALID_DATE401X-Date 5 dakikalık pencerenin dışındaSunucu saatinizi NTP ile senkronize edin
NONCE_REPLAY401Aynı nonce daha önce kullanıldıHer istekte yeni nonce üretin
INVALID_KID401Key id bulunamadı veya aktif değilPanelden anahtarın durumunu kontrol edin
MERCHANT_SUSPENDED403Hesap aktif değilDestek ile görüşün
IP_NOT_ALLOWED403İstek IP'si allow-list'te değilPanelden sunucu IP'nizi ekleyin
DIGEST_MISSING401Body var ama Digest yokBody'nin SHA-256 hash'ini ekleyin
INVALID_DIGEST401Digest gövdeyle uyuşmuyorBody'yi boşluksuz JSON olarak gönderin ve digest'i tam olarak o metin üzerinden hesaplayın
INVALID_SIGNATURE401İmza doğrulanamadıKanonik stringi kontrol edin (query string, Digest satırının varlığı)
PRODUCT_NOT_ENABLED403Anahtarın ürün listesinde cüzdan ürünü yokAnahtarın ürünlerine WALLET_SERVICE ekleyin
WALLET_INSUFFICIENT_SCOPE403Anahtarda bu çağrı için gereken cüzdan izni yokAnahtar 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:

KorumaNasıl
Tahmin edilemezlik70 bit entropi (14 karakter × 32 harfli alfabe)
Tek kullanımlıkAtomik olarak yakılır ve geri bırakılmaz — kurulum başarısız olsa bile
Kısa ömürVarsayılan 15 dakika
Kaba kuvvetIP 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 kid ile gelen istekler INVALID_KID alır.
  • Özel anahtar asla log'a, repository'ye, mobil uygulamaya veya tarayıcıya girmemelidir. Platform da özel anahtarınızı saklamaz.