Cüzdan
Webhook'lar

Webhook'lar

Cüzdanın asenkron sonuçları webhook ile bildirilir. Para hareketlerinin sonucunu polling ile değil webhook ile öğrenin; GET uçları teyit içindir.

Webhook kaydı, para hareketiyle aynı veritabanı işleminde yazılır. Yani "para tamamlandı ama olay kayboldu" durumu yapısal olarak mümkün değildir — teslimat gecikebilir, ama olay üretilmemiş olamaz.

Olay kataloğu

Bakiye yükleme

OlayNe zaman
wallet.topup.completedYükleme bakiyeye yansıdı
wallet.topup.failedYükleme başarısız
wallet.topup.refund_pendingYükleme iadesi başlatıldı
wallet.topup.refundedYükleme iadesi tamamlandı
wallet.topup.reversedTamamlanmış yükleme ters kaydedildi

Transfer

OlayNe zaman
wallet.transfer.completedTransfer tamamlandı
wallet.transfer.failedTransfer başarısız, rezervasyon çözüldü
wallet.transfer.reversedTamamlanmış transfer ters kaydedildi

Para çekme

OlayNe zaman
wallet.withdrawal.processingTalimat bankada işleniyor
wallet.withdrawal.completedÇekim tamamlandı
wallet.withdrawal.failedÇekim başarısız
wallet.withdrawal.awaiting_reconSonuç belirsiz, mutabakata alındı
wallet.withdrawal.reversedTamamlanmış çekim ters kaydedildi

Ödeme ve iade

OlayNe zaman
wallet.payment.completedCüzdanla ödeme tamamlandı
wallet.payment.failedÖdeme başarısız
wallet.payment.reversedTamamlanmış ödeme ters kaydedildi
wallet.refund.creditedCüzdana iade kredilendi
wallet.refund.reversedİade ters kaydedildi

Sadakat

OlayNe zaman
wallet.loyalty.earnedPuan kazanıldı
wallet.loyalty.redeemedPuan harcandı
wallet.loyalty.expiredPuan lotu süresi doldu
wallet.loyalty.tier_changedKullanıcının seviyesi değişti
wallet.badge.grantedRozet verildi
wallet.badge.reward_creditedRozet ödülü yatırıldı
wallet.campaign.paused_insufficient_fundsKampanya bütçesi bitti, duraklatıldı

Fatura ve fatura ödeme

OlayNe zaman
wallet.invoice.issued · paid · cancelledFatura yaşam döngüsü
wallet.billpay.dispatchedTalimat kuruma iletildi
wallet.billpay.confirmedKurum onayladı
wallet.billpay.failedKurum reddetti
wallet.billpay.awaiting_reconSonuç belirsiz
wallet.billpay.reversedTers kaydedildi

Operasyon

OlayNe zaman
wallet.suspense.createdEşleşmeyen para askıya alındı

Sağlayıcısı henüz bağlanmamış yeteneklerin olayları (çekim, fatura ödeme) katalogda yer alır ama üretimde akmaz. Tüketicinizi baştan bunlara hazırlayın; entegrasyon bağlandığında kod değişikliği gerekmez. Bkz. Yol Haritası.

Zarf

Her teslimat aynı zarfla gelir; cüzdan olayının kendi alanları data altındadır:

{
  "id": "0b6d3f2e-9a41-4c7e-8d25-6f1a3b9c4e07",
  "type": "wallet.transfer.completed",
  "sourceService": "wallet-service",
  "organizationId": "3c9a1e5b-7d2f-4b8e-a6c4-1f0e9d2b7a35",
  "productCode": "WALLET_SERVICE",
  "environment": "LIVE",
  "eventVersion": 1,
  "schemaVersion": "v1",
  "createdAt": "2026-08-07T12:00:04.512Z",
  "data": {
    "schemaVersion": "wallet.webhook.v1",
    "domain": "WALLET",
    "eventName": "wallet.transfer.completed",
    "operationId": "7a3e9c1d-2b4f-4e8a-9d6c-0f5b8e2a1c93",
    "sourceWalletAccountId": "c2a5e8f1-9d3b-4c7a-8e6f-5b1d2a9c0e47",
    "destinationWalletAccountId": "a4d7b0e3-5f8c-4a1d-9b2e-6c3f8a1d0e59",
    "amount": "10050",
    "currency": "TRY",
    "feeTotal": "250",
    "taxTotal": "125"
  }
}
AlanAnlamı
idOlay kimliği — x-lunixi-event-id header'ıyla aynı değer
typeOlay adı (yukarıdaki katalog)
organizationIdOrganizasyonunuz
environmentTEST veya LIVE
createdAtOlayın oluşturulma zamanı
dataCüzdan olayının alanları

data içinde:

AlanAnlamı
schemaVersionCüzdan payload sürümü — geriye dönük uyumsuz değişiklikte (breaking change) artar
domainHer zaman WALLET
eventNameOlay adı (yukarıdaki katalog)

data içinde bunların yanında olaya özgü alanlar taşınır (operasyon kimliği, tutarlar, durum).

⚠️

Tutarlar payload'da minor-unit taşınır ve PII maskelidir. Ham telefon numarası, IBAN veya isim webhook gövdesinde gönderilmez — bunlara ihtiyacınız varsa ilgili GET ucundan okuyun.

İmza doğrulama

Her teslimat şu header'ları taşır:

Headerİçerik
x-lunixi-event-idOlayın benzersiz kimliği
x-lunixi-event-typeOlay adı
x-lunixi-signatureVirgülle ayrılmış imza listesi: bir veya iki v2=<hex> öğesi (ikincisi yalnız 24 saatlik secret-rotasyonu grace penceresinde)
x-lunixi-signature-timestampİmza zaman damgası (ISO 8601)

v2 imzası ham gövdenin HMAC'idir — gövdeyi parse etmeden doğrularsınız:

signature = HMAC-SHA256( secret, "{eventId}.{timestamp}.{rawBody}" )

Başlıktaki her v2= öğesini bu değerle sabit zamanlı karşılaştırın; herhangi biri eşleşirse imza geçerlidir (secret rotasyonunun 24 saatlik grace penceresinde iki v2= öğesi bulunur).

import { createHmac, timingSafeEqual } from 'node:crypto';
 
export function verifyWalletWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
  const eventId   = headers['x-lunixi-event-id'];
  const timestamp = headers['x-lunixi-signature-timestamp'];
  if (Math.abs(Date.now() - Date.parse(timestamp)) > toleranceSeconds * 1000) return false;
 
  const expected = createHmac('sha256', secret)
    .update(`${eventId}.${timestamp}.${rawBody}`)
    .digest('hex');
 
  return String(headers['x-lunixi-signature'] || '')
    .split(',')
    .map((item) => item.trim())
    .filter((item) => item.startsWith('v2='))
    .some((item) => {
      const received = Buffer.from(item.slice(3), 'hex');
      const wanted = Buffer.from(expected, 'hex');
      return received.length === wanted.length && timingSafeEqual(received, wanted);
    });
}
⚠️

İmzayı doğrulamadan hiçbir webhook'u işlemeyin. Endpoint'iniz internete açıktır; imzasız gövde saldırgan tarafından üretilmiş olabilir.

Teslim garantisi

Cüzdan webhook'ları en az bir kez (at-least-once) teslim edilir:

  • Bir operasyonun durumu değiştiğinde (ör. completed, failed, reversed) ilgili olay, durum değişikliğiyle aynı işlemde kaydedilir. Operasyon bir duruma ulaştıysa o durumun olayı da üretilmiştir; altyapı kesintisinde olay kaybolmaz, kesinti bittiğinde gecikmeli olarak teslim edilir.
  • Aynı olay birden fazla kez teslim edilebilir. Her tekrar aynı x-lunixi-event-id değerini taşır; bu değer olay başına sabittir ve tekilleştirme anahtarı olarak kullanılmalıdır.
  • Olaylar gecikmeli gelebilir. Operasyonun güncel durumunu her zaman GET /wallet/operations/{id} ile teyit edebilirsiniz.

Yeniden deneme

Endpoint'iniz 2xx dönmezse teslimat yeniden denenir. Bekleme süreleri sabit bir cetvele göredir ve her değere ±%20 jitter uygulanır; deneme hakkı tükendiğinde teslimat dead letter olarak işaretlenir (DEAD_LETTER).

DavranışDeğer
Yanıt zaman aşımı~10 saniye
BeklemeSabit cetvel: 10 sn, 30 sn, 2 dk, 5 dk, 15 dk, 30 dk, 1 sa, 2 sa, 4 sa, 8 sa, 12 sa, 24 sa (±%20 jitter)
Deneme sayısı12 (~52 saatlik ufuk; aşılırsa DEAD_LETTER) — yeniden oynatma bütçeyi sıfırlar
YönlendirmeTakip edilmez — 3xx başarısızlıktır

Uzun süre hata veren bir endpoint otomatik olarak devre dışı bırakılabilir. Endpoint'inizi düzelttikten sonra panelden durumunu kontrol edin.

Doğru tüketim

  1. Hızlı 2xx dönün. İşi kuyruğa alın; webhook isteği içinde uzun iş yapmayın — zaman aşımı yeniden denemeye yol açar.
  2. Idempotent olun. Teslimat en az bir kezdir; aynı olay birden fazla kez gelebilir. x-lunixi-event-id'yi saklayın ve aynı kimlikle gelen tekrarı yok sayın.
  3. Sıra garantisi yoktur. completed olayı processing'den önce gelebilir. Durumu olayın sırasına değil içeriğine göre yazın; şüphede GET /wallet/operations/{id} ile teyit edin.
  4. Bilinmeyen olayı yok sayın. Yeni olay tipleri eklenebilir; tanımadığınız type için 2xx dönüp geçin.
  5. 3xx döndürmeyin. Yönlendirme takip edilmez.

Kurulum

Endpoint URL'nizi ve dinleyeceğiniz olayları merchant panelden tanımlarsınız; imza secret'ınız orada üretilir. Endpoint'iniz herkese açık ve HTTPS olmalıdır.

Yaygın hatalar

DurumSebepÇözüm
İmza hiç tutmuyorGövde parse edilip yeniden serileştirildiv2 imzasını ham gövde byte'ları üzerinden doğrulayın — parse etmeyin
Aynı işlem iki kez kaydedildiIdempotency yokx-lunixi-event-id ile tekilleştirin
Olaylar gelmiyorEndpoint art arda hata verdiği için pasifleşmişPanelden durumu kontrol edin
Bazı olaylar hiç gelmiyorO yeteneğin entegrasyonu bağlı değilYol Haritası