Türkiye'de sanal POS entegrasyonu yaparken vereceğiniz ilk karar hangi kart markasını destekleyeceğiniz değil, ödeme sonucunu hangi kanaldan öğreneceğinizdir. İki mimari var: iyzico'nun kullandığı senkron iki adımlı model (başlat, kullanıcıyı 3DS sayfasına gönder, tamamla) ve PayTR'ın kullandığı asenkron bildirim modeli (isteği gönder, sonucu ayrı bir sunucu-sunucu çağrısından al). Bu seçim sipariş durum makinenizi, iade akışınızı ve hangi alanları veritabanına yazacağınızı baştan belirler. Yanlış model üzerine kurulan bir ödeme akışı, üretimde çift sipariş ve kayıp ödeme olarak geri döner.
Bu yazı kart verisi işleyen ekipler için yazıldı. Bankalarla doğrudan hesap hareketi ve açık bankacılık tarafını Banka Entegrasyonu: ÖHVPS, MT940 ve Sanal POS yazımızda ele almıştık.
Asenkron Modelde "Başarılı" Sayfası Bir Kanıt Değildir
PayTR'ın Direkt API dokümantasyonu bu noktayı bir uyarı kutusuna koymuş: müşteri ödeme sonrası merchant_ok_url adresine yönlendirilir, ama o yönlendirmenin POST gövdesinde hiçbir veri yoktur. Ödemenin kesin sonucu yalnızca Mağaza Paneli'nde tanımlı Bildirim URL'ye sunucu tarafından POST edilir. Dokümantasyonun kendi ifadesiyle: "merchant_ok_url'e herhangi bir veri POST edilmemektedir, bu nedenle merchant_ok_url olarak belirttiğiniz sayfada sipariş onay/iptal gibi işlem yapmamalısınız."
Bildirim URL'de uyulması gereken kurallar oldukça katı:
- Yanıt düz metin ve yalnızca
OKolmalıdır. Öncesinde veya sonrasında HTML basarsanız bildirim başarısız sayılır ve işlem panelde "Devam Ediyor" olarak kalır. - Sayfa oturum kullanamaz. Bu sayfaya müşteri gelmez, PayTR sunucusu gelir. Siparişi
merchant_oidile bulursunuz. - Aynı ödeme için birden fazla bildirim gelebilir. Yalnızca ilki işlenir; sonrakilere sadece
OKdönülür. Tekrar tespitimerchant_oidüzerinden yapılır. - POST içindeki
hashdeğeri mutlaka doğrulanır. Dokümantasyonun uyarısı net: bu kontrolü yapmazsanız maddi kayıpla karşılaşabilirsiniz.
Token üretimi de tahmin edilebilir bir sırayla yapılır; alan sırasını değiştirmek imzayı bozar:
// PHP, PayTR Direkt API 1. Adım
$hash_str = $merchant_id . $user_ip . $merchant_oid . $email
. $payment_amount . $payment_type . $installment_count
. $currency . $test_mode . $non_3d;
$token = base64_encode(
hash_hmac('sha256', $hash_str . $merchant_salt, $merchant_key, true)
);
Dikkat edilmesi gereken bir ayrıntı: 1. adımda gönderdiğiniz payment_amount ondalıklı gelir (100.99), ama callback'te aynı alan 100 ile çarpılmış tam sayı olarak döner (34.56 ise 3456). Tutar karşılaştırmasını bunu bilmeden yazarsanız her ödeme "tutar uyuşmuyor" diye reddedilir.
Senkron Modelde İş, İki POST ve Bir Base64 Çözmeye İner
iyzico'nun 3DS akışı altı adımdan oluşuyor: BIN sorgulama, 3DS başlatma, threeDSHtmlContent çözme, yönlendirme, 3DS tamamlama, webhook. Servis adresleri sabittir:
POST https://api.iyzipay.com/payment/3dsecure/initialize
POST https://api.iyzipay.com/payment/3dsecure/auth // v1
POST https://api.iyzipay.com/payment/v2/3dsecure/auth // v2, HMACSHA256 sonrası önerilen
Header: Authorization: IYZWSv2 <base64 imzalı hash>
Başlatma isteğinin zorunlu alanları şunlar: price, paidPrice, callbackUrl, paymentCard, buyer, shippingAddress, billingAddress, basketItems. Sepetteki ürünlerden en az biri itemType: "PHYSICAL" ise kargo adresi zorunludur; hepsi VIRTUAL ise gönderilmez. Yanıtta gelen threeDSHtmlContent, kullanıcıya gösterilecek 3DS ekranının Base64 kodlanmış HTML'idir; çözüp tarayıcıya basarsınız.
price ile paidPrice ayrımını yanlış kuran ekipler çoktur. price sepet toplamı, paidPrice tahsil edilecek nihai tutardır. Aradaki fark üye işyerinin uyguladığı vade farkıdır ve yanıtta merchantCommissionRate ile merchantCommissionRateAmount olarak bilgi amaçlı geri döner. Taksitli satışta vade farkını price'a eklerseniz komisyon raporlarınız tutmaz.
mdStatus: tek başarı değeri, dokuz başarısızlık değeri
3DS tamamlama yanıtındaki mdStatus, doğrulamanın sonucunu özetler. Dokümantasyona göre başarılı tek değer 1'dir:
| mdStatus | Anlamı | Kullanıcıya ne dersiniz |
|---|---|---|
1 | Doğrulama başarılı | Ödemeyi tamamla |
0 | 3-D Secure imzası geçersiz veya doğrulanamadı | Tekrar denemesini iste |
-1 | 0 ile aynı; QNB Finansbank'a özel dönüş | 0 ile aynı şekilde işle |
2 | Kart sahibi veya bankası sisteme kayıtlı değil | Farklı kart önermek mantıklı |
3 | Kartın bankası sisteme kayıtlı değil | Farklı kart önermek mantıklı |
4 | Doğrulama denemesi; kart sahibi sonra kayıt olmayı seçmiş | Tekrar dene |
5 | Doğrulama yapılamıyor | Bankaya yönlendir |
6 | 3-D Secure hatası | Tekrar dene |
7 | Sistem hatası | Sonra tekrar dene |
8 | Bilinmeyen kart numarası | Kart numarasını kontrol ettir |
Bu tabloyu tek bir "ödeme başarısız" mesajına indirgemeyin. mdStatus 2 ve 3, kullanıcının yapabileceği bir şey olduğunu söyler. mdStatus 7, kullanıcının yapabileceği hiçbir şey olmadığını söyler. İkisine aynı ekranı göstermek, dönüşüm oranınızdan sessizce puan götürür.
fraudStatus 0 gördüğünüzde ürünü göndermeyin
3DS tamamlama yanıtında fraudStatus alanı üç değer alır: 1 onaylandı, 0 incelemede, -1 reddedildi. Dokümantasyonun ifadesi kesin: üye işyeri yalnızca 1 olan işlemlerde ürünü kargoya vermelidir, 0 olan işlemler için bilgilendirme beklenmelidir. Sipariş durum makinenizde "ödeme alındı" ile "kargoya verilebilir" ayrı iki durum değilse, bu alan işinize yaramaz.
3-D Secure 2.x Neyi Değiştirdi
EMVCo'nun tanımıyla EMV 3DS, kart sahibinin bulunmadığı (CNP) alışverişlerde tüketici doğrulaması sağlayan bir e-ticaret dolandırıcılık önleme protokolüdür. 2.x ile gelen esas fark, doğrulamanın artık her işlemde şifre sormak zorunda olmaması: işlem, cihaz ve alışveriş verisi kart çıkaran kuruma gönderilir, kurum riski düşük görürse doğrulama frictionless tamamlanır, yüksek görürse challenge akışına düşer. EMVCo, PSD2 uyumu için 2.2 ve üzeri sürümleri işaret ediyor; 2.4 sürümü hâlihazırda taslak aşamasında.
Türkiye'de üye işyeri tarafında bu ayrıntıların çoğunu görmezsiniz. Ödeme kuruluşu AReq/ARes ve CReq/CRes mesajlaşmasını sizin adınıza yürütür, size özetlenmiş bir sonuç döner. Ama iki sonuç doğrudan bu katmandan gelir ve tanımanız gerekir:
10210INVALID_CAVV: doğrulama değeri geçersiz. Yanlış ortam anahtarı ya da bozuk 3DS oturumu ilk bakılacak yerdir.10211INVALID_ECI: e-ticaret gösterge değeri hatalı. Kart kaynaklıdır, kullanıcının bankasıyla görüşmesi gerekir.
Taksit Tablosunu BIN Sorgusu Olmadan Göstermeyin
iyzico'da taksit sayısı installment alanıyla gönderilir ve şemadaki izinli değerler 1, 2, 3, 4, 6, 9, 12'dir. PayTR'da alan installment_count'tur ve 0, 2, 3, ... 12 değerlerini alır; ayrıca taksitli işlemde card_type gönderilmesi gerekir ve izinli değerler kart programlarının adlarıdır: advantage, axess, combo, bonus, cardfinans, maximum, paraf, world, saglamkart. PayTR bu değeri BIN sorgu servisinden dönen brand alanından almanızı söylüyor.
Burada gerçek bir tuzak var. iyzico'nun servis limitleri tablosunda BIN sorgulama (/payment/bin/check) limiti dakikada 50 istek. Ödeme formunuzda kullanıcı kart numarasının ilk altı hanesini yazdıkça sorgu atan bir "canlı taksit tablosu" yazdıysanız, orta ölçekli bir kampanya gününde bu limite çarparsınız. Limit aşımında dönen yanıt şudur:
{
"status": "failure",
"errorCode": 50000,
"errorMessage": "Request Limit Exceeded",
"locale": "en"
}
Çözüm karmaşık değil: BIN sonuçlarını BIN + tutar anahtarıyla kısa süreli önbelleğe alın ve sorguyu kullanıcı altı haneyi tamamladığında tek sefer tetikleyin. Her tuş vuruşunda değil.
İade ve İptal Aynı Şey Değil, Aynı Kimliği de Kullanmıyorlar
Bu, sanal POS entegrasyonlarında en çok yanlış kurgulanan yer. iyzico dokümantasyonuna göre ayrım şöyle:
| İşlem | Endpoint | Anahtar alan | Kısmi? | Dakikalık limit |
|---|---|---|---|---|
| İptal | /payment/cancel | paymentId | Hayır | 150 |
| İade | /payment/refund | paymentTransactionId | Evet | 400 |
| İade V2 | /v2/payment/refund | paymentId | Evet | 150 |
Üç nokta altını çizmeye değer. Birincisi, iptal kısmi tutarı desteklemez ve kart ekstresinde giriş/çıkış kaydı oluşturmaz; iade ise ekstreye yansır. İkincisi, klasik iade paymentId ile değil paymentTransactionId ile, yani sepet kırılımı bazında yapılır. Üç kalemlik bir siparişte üç ayrı paymentTransactionId vardır ve bunları saklamadıysanız kalem bazlı iade yapamazsınız. Üçüncüsü, ödeme 365 gün içinde iade edilebilir; iade tutarı ilgili kırılımın paidPrice değerinden ve kalan iade edilebilir tutardan büyük olamaz, ama bu kurala uyulduğu sürece art arda birden fazla kısmi iade yapılabilir.
İade ve iptal isteklerinde reason alanı dört değerden birini alır: OTHER, FRAUD, BUYER_REQUEST, DOUBLE_PAYMENT. Açıklama gönderiyorsanız neden zorunlu hâle gelir. Yanıtta dönen retryable alanını da kaydedin; başarısız bir iadeyi körlemesine tekrar denemek yerine bu bayrağa bakmak, destek ekibinizin saatlerini geri kazandırır.
Burası Kırılıyor: Ödeme API'leri Idempotent Değil
iyzico'nun kendi dokümantasyonunda açıkça yazıyor: "iyzico hizmetlerinin çoğu, tekrarlanan taleplerde öngörülebilir ve tutarlı davranış sağlamak için idempotent olmayan şekilde tasarlanmıştır." Aynı isteği iki kez gönderirseniz iki ödeme oluşur. Bunu engelleyen sihirli bir Idempotency-Key başlığı yok.
Korunma yükü sizde ve iki yerde birden kurulması gerekir:
- İstek tarafı: Her ödeme girişimine kendi ürettiğiniz tekil bir
conversationId(ve/veyabasketId) verin, bunu göndermeden önce veritabanına yazın. Aynı sepet için açık bir girişim varsa ikinciyi engelleyin. - Yanıt tarafı: Dönen
paymentIdve her kırılım içinpaymentTransactionIddeğerlerini yanıtı işlerken saklayın. Bu iki alan olmadan ne iade yapabilirsiniz ne de sağlayıcıyla işlem özelinde yazışabilirsiniz.
PayTR tarafında aynı problem farklı bir kılıkta karşınıza çıkar: ağ sorunları ya da anlık yoğunluk nedeniyle aynı ödeme için birden fazla bildirim gelebilir. Doğru davranış, siparişin durumunu önce veritabanından kontrol etmek, onaylanmışsa tekrar işlem yapmadan sadece OK dönmektir. Bunu yapmayan bir entegrasyonda aynı müşteriye aynı ürün iki kez gönderilir.
PayTR başarısızlık kodları: hepsi "kart reddetti" değil
| failed_reason_code | Gerçekte ne oldu |
|---|---|
1 | Müşteri kimlik doğrulama adımında telefon numarasını girmedi |
2 | SMS şifresi yanlış girildi |
3 | PayTR güvenlik kontrolünden geçemedi veya kontrol yapılamadı |
6 | Müşteri süreyi doldurdu ya da sayfayı kapattı (request_exp_date) |
8 | Bu karta taksit yapılamıyor |
9 | Mağazanın bu kart için işlem yetkisi yok |
10 | Bu işlemde 3D Secure kullanılmalı |
11 | Fraud tespiti var; müşteriyi kontrol edin |
99 | Teknik entegrasyon hatası (debug_on=0 iken dönen genel kod) |
Kod 6 ile kod 2'yi aynı ekranda toplamak, en sık kaçırılan dönüşüm fırsatıdır. Kod 6, müşteri hâlâ satın almak istiyor ama vazgeçmiş demektir; sepeti canlı tutup hatırlatma göndermek işe yarar. Kod 2 ise anlık bir tekrar denemeye açıktır.
Fraud kodlarında müşteriye detay vermeyin
iyzico'nun banka hata kodları listesi, bazı kodlarda alınacak aksiyonu açıkça yazıyor. 10034 FRAUD_SUSPECT, 10041 PICKUP_CARD / LOST_CARD ve 10043 STOLEN_CARD için tavsiye net: müşteriye bilgi verilmemesi ve bankaya yönlendirilmesi. Kullanıcıya "kartınız çalıntı olarak işaretli" yazan bir ekran, gerçekten çalıntı kart kullanan kişiye yardım eder. Genel bir mesaj gösterin ve işlemi kayda alın.
Gündelik hayatta en sık göreceğiniz kodlar ise sıkıcı olanlardır: 10051 yetersiz bakiye, 10054 son kullanma tarihi hatalı, 10084 CVC hatalı, 10093 kart internetten alışverişe kapalı. Son kod için kullanıcıya bankasının mobil uygulamasından kartı internet alışverişine açabileceğini söylemek, çağrı merkezi yükünüzü gözle görülür biçimde düşürür.
Kart Verisi: Sunucunuza Asla Uğramasın
PayTR'ın dokümantasyonundaki uyarı, mimari bir kural olarak okunmalı: "Üye iş yeri sayfasındaki form, kart bilgileri içerdiğinden sadece PayTR'a POST edilmelidir. Üye iş yerinin kendi sunucusuna POST kesinlikle yapılmamalıdır."
Mevzuat tarafında bu verinin adı var. TCMB'nin 1 Aralık 2021 tarihli ve 31676 sayılı Resmî Gazete'de yayımlanan Ödeme Hizmetleri ve Elektronik Para İhracı ile Ödeme Hizmeti Sağlayıcıları Hakkında Yönetmeliği, 3'üncü maddesinde hassas müşteri verisini şöyle tanımlıyor: "Ödeme emrinin verilmesinde veya müşterinin kimliğinin doğrulanmasında kullanılan ve üçüncü kişilerce ele geçirilmesi veya değiştirilmesi halinde dolandırıcılık ya da müşteri adına sahte işlem yapılmasına imkân verebilecek kişisel veriler ile müşteri güvenlik bilgileri."
Pratik karşılığı: kart numarası, CVC ve son kullanma tarihi sizin uygulama loglarınıza, hata izleme aracınıza veya veritabanınıza hiç girmemeli. Tekrar eden ödeme ihtiyacınız varsa saklama işini sağlayıcının kart saklama servisine bırakıp elinizde yalnızca token tutun. Güvenlik yaklaşımımızı güvenlik merkezi sayfasında bulabilirsiniz.
Hangi Modeli Seçmelisiniz
| Durum | Seçim | Neden |
|---|---|---|
| Ödeme formunu tamamen kendi tasarımınızla göstermek istiyorsunuz | Doğrudan API + 3DS | Tam kontrol, ama kart verisini hiç dokunmadan iletmek sizin sorumluluğunuz. |
| Hızlı çıkmak istiyorsunuz, tasarım ikincil | Barındırılan ödeme formu | Kart verisi sizin alan adınıza hiç girmez; doğrulama ve taksit ekranı hazır gelir. |
| Kalem bazlı iade ve pazaryeri hakediş dağıtımı gerekiyor | Kırılım bazlı model | paymentTransactionId ve alt üye işyeri alanları olmadan bu akış kurulamaz. |
| Mevcut ERP'niz sipariş durumunu asenkron işliyor | Bildirim tabanlı model | Sunucu-sunucu bildirimi zaten var olan kuyruk yapınıza doğal oturur. |
| Abonelik veya tekrarlayan tahsilat yapacaksınız | Kart saklama + token | Her tahsilatta 3DS mümkün olmaz; saklı kart akışı ve başarısızlık yönetimi ayrı tasarlanır. |
Üretime Çıkmadan Önceki Yedi Kontrol
- Callback / bildirim uç noktanız oturumsuz çalışıyor ve yalnızca
OKya da beklenen yanıtı dönüyor. - Aynı sipariş için gelen ikinci bildirim, yeni bir işlem üretmeden yanıtlanıyor.
- Gelen imza (
hashveyasignature) her seferinde doğrulanıyor; doğrulanamayan bildirim işlenmiyor. paymentIdve tümpaymentTransactionIddeğerleri kaydediliyor.fraudStatus1 değilse kargo süreci tetiklenmiyor.- BIN sorguları önbellekli; dakikada 50 limitine çarpma ihtimali ölçülmüş.
- Kart numarası, CVC ve son kullanma tarihi hiçbir log satırında görünmüyor. Bunu bir test ile kanıtlayın, varsayımla geçmeyin.
Bu yedi maddenin hepsi yeşilse entegrasyonunuz üretime hazırdır. Ödeme altyapısı seçimi ve mimarisi konusunda destek isterseniz ödeme sistemi entegrasyonları sayfamıza, sektörel yaklaşımımız için finans ve fintech sayfamıza bakabilir, kendi kapsamınız için bir adam-gün bandı görmek isterseniz proje fiyat hesaplama aracını kullanabilirsiniz.
Bu yazıdaki alan adları, endpoint'ler, limitler ve hata kodları 25 Eylül 2026 tarihinde sağlayıcıların resmî geliştirici dokümantasyonundan doğrulanmıştır: iyzico için docs.iyzico.com (3DS Başlatma/Tamamlama OpenAPI şemaları, İptal ve İade, Limitler, Eşleştirme, Hata Kodları sayfaları), PayTR için dev.paytr.com (Direkt API 1. ve 2. Adım). EMV 3-D Secure tanımı EMVCo'nun emvco.com adresindeki 3-D Secure sayfasından, hassas müşteri verisi tanımı 01.12.2021 tarihli 31676 sayılı Resmî Gazete'de yayımlanan TCMB Yönetmeliğinin 3'üncü maddesinden alınmıştır. Sağlayıcı dokümantasyonları sık güncellenir; üretime çıkmadan önce limit ve şema bilgilerini teyit edin.