iyzico — iyzico (PayU Türkiye)
> Tip: Ödeme kuruluşu (PSP) — BDDK lisanslı, kart işlemcisi rolünde > Adapter'lar: InvumPosGatewayIyzico* > Son senkron: 2026-05-04 (Hafta 14 maratonu — 3PG decrypt + paymentTransactionId persist) > Last-known-good API: IYZWSv2 — checkout-form akışı (2026-Q2 dev portal sürümü)
iyzico'nun iki ayrı API ailesi var ve bunlar farklı endpoint'lerde yaşıyor. Bu ayrımı bilmek kritik — önceki haftalarda errorCode 5087 ile saatler yedik (bkz. quirk #1):
- Direct API ailesi (
/payment/auth,/payment/detail, …) — kart bilgileri
bizim sayfada toplanıp doğrudan iyzico'ya postlandığı PCI-DSS akışı.
- Checkout Form ailesi (
/payment/iyzipos/checkoutform/...) — iyzico'nun kendi
hosted form'u; biz token üretip yönlendiriyoruz, kart iyzico tarafında toplanıyor. Bizim aktif kullandığımız akış bu.
1. Bağlantı bilgileri
| Ortam | Base URL | Notlar | |————|—————————————|———————————————| | Sandbox | https://sandbox-api.iyzipay.com | Sandbox merchant signup ücretsiz | | Production | https://api.iyzipay.com | Sözleşme + ticari hesap doğrulama |
Sandbox merchant panel: https://sandbox-merchant.iyzipay.com Production merchant panel: https://merchant.iyzipay.com Dev portal: https://dev.iyzipay.com (resmî dokümantasyon, changelog yok) Developer blog: https://developer.iyzico.com (TR — duyurular bazen burada) Mailing list: Yok. Duyurular merchant panele ve hesap email'ine düşer (cenk@invum.com.tr). Yetkili temsilci: TODO (production sözleşmesi imzalandığında atanır)
2. Auth şeması — IYZWSv2
Her HTTP isteği için header üretimi (clean-room açıklama, public spec):
randomKeyüret — millisecond timestamp + 8 byte random hex (örn.1700000000000abc1234)payload=randomKey . uri . jsonBody(string concat, normalize yok)signature=hmac_sha256(payload, secret_key)→ hex çıktıauthParams=apiKey:<key>&randomKey:<rk>&signature:<sig>(& ile join, URL-encode YOK)Authorizationheader =IYZWSv2+base64(authParams)
Örnek header seti:
Authorization: IYZWSv2 <base64-encoded-authparams>
x-iyzi-rnd: <randomKey>
x-iyzi-client: invum-pos/<version>
Content-Type: application/json
Accept: application/json
Sırlar (encrypted store): api_key, secret_key. WCGateway process_admin_options AES-256-CBC ile saklıyor; RefundService::bootstrap decrypt ediyor (Hafta 14 fix). Plain-text geriye uyumluluk: decrypt fail ederse orijinal değer kullanılır.
Önemli: randomKey her istekte taze olmalı; aynı randomKey ile 2 istek atarsanız iyzico ikinci'yi reddediyor (replay protection).
3. Endpoint tablosu (kullandıklarımız)
| İşlem | Method | Path | İstek field'ları (kritik) | Yanıt field'ları (kritik) | |————————-|——–|———————————————————–|——————————————————————-|———————————————————————-| | Charge (form init) | POST | /payment/iyzipos/checkoutform/initialize/auth/ecom | conversationId, price, paidPrice, callbackUrl, basketItems[], buyer, billingAddress, shippingAddress | status, token, paymentPageUrl, checkoutFormContent | | Callback verify + status| POST | /payment/iyzipos/checkoutform/auth/ecom/detail | token | status, paymentStatus, paymentId, basketId, itemTransactions[], paidPrice | | Refund | POST | /payment/refund | conversationId, paymentTransactionId, price, currency, ip | status, paymentId | | Cancel (kullanmıyoruz) | POST | /payment/cancel | conversationId, paymentId, ip | status | | BIN-installment | POST | /payment/iyzipos/installment | conversationId, binNumber, price | installmentDetails[].installmentPrices[] |
/payment/detail endpoint'i bizim akışta YOK — bkz. quirk #1.
Test kartları (sandbox):
5528790000000008— Halkbank Bonus, başarılı4543600000000006— Akbank Axess, başarılı5400360000000003— Garanti Bonus, başarılı (taksitli)4604370000000004— auth fail testi (insufficient funds)5170410000000004— 3DS fail testi- CVV:
123, exp:12/30(gelecekte herhangi bir tarih)
4. Bilinen quirk'ler
#1 — errorCode 5087 "Üye işyerine ait ödeme kaydı bulunamadı"
Davranış: Checkout-form akışı ile yapılan charge'lar /payment/detail (Direct API ailesi) endpoint'inde bulunamaz. Detail sorgusu 5087 döner.
Kök neden: İki API ailesi farklı veritabanı tablolarına yazıyor. Checkout-form ödemesi /payment/iyzipos/checkoutform/auth/ecom/detail ile sorgulanmalı; Direct API ödemesi /payment/detail ile.
Çözüm: Refund için ihtiyaç duyulan paymentTransactionId'yi callback sırasında yakalıyoruz (checkout-form detail yanıtının itemTransactions[0] içinde geliyor) ve wp_invum_pos_transactions.payment_transaction_id kolonuna saklıyoruz (migration 1.4.0). Refund anında DB'den okuyoruz, runtime detail çağrısı yok. Eski (pre-1.4.0) kayıtlar için wp invum-pos refund-anchor set WP-CLI komutu manuel doldurma sağlıyor.
#2 — errorCode 5092 "Bu işyerine ait ödeme kırılım kaydı bulunamadı"
Davranış: /payment/refund çağrısında paymentTransactionId alanına top-level paymentId gönderirsen 5092 dönüyor.
Kök neden: iyzico refund'u per-item seviyesinde yapıyor. paymentTransactionId her basket item için ayrı bir ID; paymentId ise ödemenin tamamı. Sektörel norm "paymentId = paymentTransactionId" iyzico'da geçersiz (yanılgı maliyeti: Hafta 14 oturumunda saatler).
Çözüm: Quirk #1'in çözümüyle aynı — DB'de payment_transaction_id sakla, refund'da onu kullan.
#3 — errorCode 1001 "Api bilgileri bulunamadı"
Davranış: Hem charge hem refund 1001 ile reddediliyor.
Kök neden 1: API key/secret yanlış girilmiş. Sandbox/production karışmış olabilir (sandbox key'i prod URL'e veya tersi).
Kök neden 2: Credentials encrypted store edilmiş ama decrypt yapılmadan adapter'a geçilmiş. Hafta 14'te tam olarak bu yaşandı: RefundService::bootstrap() 88-char base64 ciphertext gönderiyordu.
Çözüm: RefundService::bootstrap() Encryption injection ile decrypt ediyor; plain-text geriye uyumluluk için fail durumunda orijinal değer kullanılır.
#4 — Multi-basket-item refund desteklenmiyor
Davranış: Bir order'da birden fazla basket item varsa itemTransactions[] 1'den fazla eleman içerir; her biri kendi paymentTransactionId, price, paidPrice taşır.
Kök neden: WC process_refund() tek tutar geçiyor, biz hangi item'ın ne kadar iade edileceğini bilmiyoruz.
Çözüm (geçici): IyzicoRefund::refund() ve callback handler ilk item'ı kabul edip diğerlerinde bail ediyor ("Multi-basket-item refund unsupported in this build (Faz 1.5)").
Faz 1.5 çözüm: Pro-rata split — total amount × (item.price / order.total) formülüyle her item için ayrı refund call'u zincirle.
#5 — payment_complete($transaction_id) paymentId'yi atıyor
WC'nin native payment_complete() metoduna iyzico'nun paymentId'sini geçiyoruz; bu, WC order meta _transaction_id olarak saklanıyor. Refund sırasında wp_invum_pos_transactions tablosundan okuyoruz, _transaction_id meta'sını okumuyoruz — bu bilinçli bir karar (transactions tablosu audit trail için canonical kaynak).
#6 — Türkçe locale zorunlu (Türkiye sandbox'ı için)
locale=tr göndermezsen errorMessage'lar İngilizce gelir; 5092 vb. hata mesajları çeviri tablosu farklı olabiliyor. Hep locale=tr yolluyoruz.
#7 — paidPrice vs price ayrımı
price = orijinal sepet toplamı, paidPrice = müşterinin ödediği tutar (taksit komisyonu varsa farklı olabilir). Şu an ikisini de eşit gönderiyoruz (amount); taksit komisyonu desteği gelirse ayrılması gerekir (Faz 2).
5. Bizim implementasyon notları
- WCGateway:
IyzicoWCGateway— sluginvum_pos_iyzico, settings'te
api_key, secret_key, test_mode (yes/no) field'ları. Encrypted: api_key, secret_key.
- Charge + callback verify + status:
IyzicoGateway
– charge() → checkout-form initialize → payment_page_url döndürür – validate3DCallback(['token' => ...]) → detail call → payment_id, payment_transaction_id, order_id – getStatus($token) → aynı detail endpoint
- Refund:
IyzicoRefund(RefundInterface)
– DB'de payment_transaction_id doluysa direkt kullan – Boşsa /payment/detail fallback (yalnız Direct API charge'larında çalışır; checkout-form için 5087 dönüyor → manuel set'e yönlendir)
- StatusQuery:
IyzicoStatusQuery— pending order cron'u için - Callback REST endpoint:
POST /wp-json/invum-pos/v1/iyzico/callback,
IyzicoCallbackController. iyzico browser üzerinden form-POST yapıyor; başarı/başarısızlık sonrası wp_safe_redirect ile WC'ye dönüş.
Persist edilen field'lar (wp_invum_pos_transactions):
transaction_id= iyzicopaymentId(üst-seviye)payment_transaction_id= iyzicoitemTransactions[0].paymentTransactionId(per-item, refund anchor) — migration 1.4.0raw_response=validate3DCallbackdönüşü, masked (PayloadMasker)
Faz 1.5+ backlog:
- Multi-basket-item refund (pro-rata split)
paidPrice≠pricedesteği (taksit komisyonu)- BIN tabanlı taksit cache'i (request başına /installment çağrısı pahalı)
- Direct API akışına opt-in (
enable_direct_apiflag) — daha az redirect, daha
fazla PCI yükü
6. Test connection (Pro modülü)
CredentialPresenceTest ne kontrol ediyor:
- API key + secret key boş mu (presence)
- Sandbox/production toggle ile URL match'i
Eksik (Faz 2): Gerçek HTTP ping. Şu an test connection sadece "credential girilmiş mi" diyor; "iyzico'ya gerçek charge denemesi yap" modu yok. Faz 2'de minimum-amount sandbox charge + cancel ile uçtan uca doğrulama eklenecek.
7. Card Storage / Vault (Hafta 15 — uygulandı)
> Durum (2026-05-05): Standalone vault + saved-card 3DS charge shipped. > F4-iyzico commit zinciri tamamlandı (Hafta 15 — sandbox E2E doğrulandı). > IyzicoCardVault (tokenize/charge_with_saved_card/delete) + Iyzico3DSBridge > + IyzicoCallbackController::detect_callback_kind saved-card path. Kart > token'ı iyzico tarafında durur, biz sadece (cardUserKey, cardToken) > çiftini saklıyoruz. PCI scope için bkz. §7.5. > Hâlâ açık (Faz 2): "Bu kartı kaydet" save-during-checkout — hosted > checkoutForm bunu desteklemiyor (bkz. quirk #V0); Direct mode lazım. > PayTR Direct (F5) bunu zaten yaptı, aynı PCI yatırımı paylaşılır.
iyzico Card Storage Direct API ailesinin parçası: aynı IYZWSv2 HMAC, aynı base URL'ler. IyzicoSignature + IyzicoHttpClient aynen kullanılır. Yeni sır gerekmez. Ancak charge için artık checkout-form değil, Direct API 3DS init kullanılacak — yeni IyzicoDirect3dsGateway adapter'ı + bizim sayfamızda kart formu (PCI SAQ A-EP scope'a giriş; bkz. §7.5).
7.1. Endpoint tablosu
| İşlem | Method | Path | İstek field'ları (kritik) | Yanıt field'ları (kritik) | |———————————|——–|————————————-|——————————————————————————————–|————————————————————————| | Kart sakla (yeni user) | POST | /cardstorage/card | email, card.cardNumber, card.expireMonth, card.expireYear, card.cardHolderName | cardUserKey, cardToken, binNumber, lastFourDigits, cardType, cardAssociation, cardFamily, cardBankName, cardAlias | | Kart ekle (mevcut user) | POST | /cardstorage/card | cardUserKey, card.* | aynı (yeni cardToken döner) | | Saklı kartları listele | POST | /cardstorage/cards | cardUserKey | cardDetails[] — her item: cardToken, cardAlias, binNumber, lastFourDigits, expireMonth, expireYear, cardType, cardAssociation, cardFamily, cardBankName | | Saklı kart sil | DELETE | /cardstorage/card | cardUserKey, cardToken | status | | 3DS init (saklı kart ile) | POST | /payment/3dsecure/initialize | paymentCard.cardUserKey, paymentCard.cardToken (raw kart YOK) + standart price/paidPrice/buyer/basketItems/billingAddress/shippingAddress | threeDSHtmlContent (base64) | | 3DS auth finalize | POST | /payment/3dsecure/auth | paymentId, conversationData | paymentId, itemTransactions[], fraudStatus | | 3DS init + tokenize at first | POST | /payment/3dsecure/initialize | paymentCard.{cardHolderName,cardNumber,expireMonth,expireYear,cvc, registerCard:1} + buyer.id (deterministik) | callback'te cardUserKey, cardToken (success durumunda) |
Ortak istek envelope'u: locale=tr, conversationId (UUID). Opsiyonel: externalId (wc_user_<id>), cardAlias (UI label).
7.2. Akış — Tokenize during 3DS charge (önerilen)
1. Checkout: kullanıcı "Kartımı kaydet" checkbox'ını işaretler
2. POST /payment/3dsecure/initialize
body.paymentCard = { cardHolderName, cardNumber, expireMonth, expireYear, cvc, registerCard: 1 }
body.buyer.id = "WC_USER_<wc_user_id>" ← deterministik (quirk #V1)
3. iyzico → threeDSHtmlContent → tarayıcıda render
4. 3DS callback /payment/3dsecure/auth → response içinde:
- paymentId, itemTransactions[].paymentTransactionId (mevcut akıştaki gibi)
- cardUserKey, cardToken, binNumber, lastFourDigits, cardAssociation
5. wp_invum_pos_saved_cards → INSERT (user_id, gateway, cardUserKey, cardToken, ...)
6. Sonraki checkout: vault'tan oku → POST /payment/3dsecure/initialize
body.paymentCard = { cardUserKey, cardToken } (raw kart YOK; CVC opsiyonel)
7.3. Akış — Standalone "Kartlarım" sayfası
1. WC My Account → "Kayıtlı kartlarım" tab
2. POST /cardstorage/cards { cardUserKey } → cardDetails[] listele
3. "Kart Ekle" formu → POST /cardstorage/card { cardUserKey, card.* }
↳ İlk eklemede `cardUserKey` boş bırakılır → iyzico yeni key üretir
↳ Sonraki eklemelerde mutlaka mevcut `cardUserKey` gönderilir (quirk #V1)
4. "Sil" butonu → DELETE /cardstorage/card { cardUserKey, cardToken }
7.4. Vault-specific quirk'ler
#V0 — Hosted checkoutForm save-during-checkout DESTEKLEMİYOR
iyzico'nun /payment/iyzipos/checkoutform/initialize/auth/ecom (hosted form) akışı — bizim Hafta 14'ten beri kullandığımız default — "Bu kartı kaydet" checkbox'ını göstermez. UI'da görünür hale getirmek için request body'de cardUserKey zorunlu; ilk kez ödeme yapan kullanıcıda cardUserKey yok → chicken-and-egg.
Sandbox doğrulaması: Mayıs 2026 sandbox'ında enabledInstallments + buyer + tam bir basketItems ile yapılan checkoutForm.initialize çağrısında hosted form sadece kart-input gösterdi, save checkbox yoktu (kullanıcı: cenk@invum.com.tr).
Gurmepos-pro v6 referansı da bunu doğruluyor: iyzico-iframe/ (hosted form) gateway'inde save_card/registerCard/cardUserKey hiç kullanılmıyor. Save UX yalnızca iyzico/ (3DS Direct API) gateway'inde — paymentCard.registerCard=1 ile.
Sonuç: Hosted form akışında save-during-checkout yapılamaz. İki yol var:
- Standalone "Yeni Kart Ekle" formu (mevcut, F4-iyzico-3a/b):
müşteri My Account → Ödeme Yöntemleri → Yeni Kart Ekle ile /cardstorage/card üzerinden kart kaydeder. Ödeme akışıyla bağı yok.
- 3DS Direct mode (Faz 2): hosted form yerine PAN bizim
formumuzda toplanır, paymentCard.registerCard=1 ile charge edilir. PCI scope SAQ A → SAQ A-EP'ye çıkar. PayTR Direct API (F5) ile aynı PCI yatırımı paylaşılır.
F4-iyzico-3-V3 (commit e0b5379) durumu: validate3DCallback response'undaki cardToken/cardUserKey alanlarını parse ediyor + maybe_persist_saved_card_during_checkout helper'ı persist ediyor. Hosted form bu alanları göndermediği için şu an dead code — Faz 2'de Direct mode geldiğinde otomatik aktif olacak. Kalsın diye karar verildi: kod doğru, sadece tetikleyici yok.
#V1 — cardUserKey deterministik değil, biz korumalıyız
iyzico ilk POST /cardstorage/card çağrısında yeni bir cardUserKey üretir (buyer.id ile değil email ile bağlar). Aynı email ile ikinci çağrı yaparsak yeni cardUserKey mi, eski mi döner — public dokümante edilmemiş.
Çözüm: İlk başarılı kayıttaki cardUserKey'i wp_invum_pos_saved_cards.gateway_user_token (UNIQUE per user_id+gateway) olarak persist et. Sonraki kart eklemelerde her zaman "ek kart" varyantını ({cardUserKey, card}) kullan; "yeni user" varyantını bir daha çağırma.
#V2 — Saklı kartla ödeme 3DS gerekir mi? (BDDK)
iyzico /payment/auth (NON3D) saklı kartla teknik olarak çalışıyor. Ancak BDDK kararı gereği TR-issued kartlarda 3DS aslında zorunlu; iyzico merchant tipine göre NON3D'yi reddedebiliyor.
Karar: Saklı kart akışını da /payment/3dsecure/initialize üzerinden kuracağız (raw kart yerine paymentCard: {cardUserKey, cardToken}). Bir 3DS ekranı UX'i biraz bozar ama BDDK uyumlu kalırız.
#V3 — CheckoutForm sayfasında registerCard flag'i public spec'te yok
iyzico'nun CFInitializeRequest schema'sı cardUserKey, registerCardEnabled, registerCard field'larını dokümante etmiyor. Hosted form'da "kartı kaydet" checkbox'ı görünüyor ama merchant tarafından nasıl toggle'landığı belirsiz.
Karar: Vault feature'ı Direct API 3DS akışıyla (yeni adapter IyzicoDirect3dsGateway) ekleyeceğiz; checkout-form akışı tokenize için kullanılmayacak. Faz 2'de iyzico ticket açılıp resmi cevap alınacak — varsa CheckoutForm üzerinden tokenize PCI scope'umuzu küçültür.
#V4 — cardToken expiry / rotasyon
Public spec TTL belirtmiyor. Pratik kural: kart vade dolduğunda cardToken otomatik geçersizleşir, charge errorCode 3006 (cardToken not found) döner.
Çözüm: Vault tablosunda expires_at = expire_year-expire_month-01 tutup vade ayında kullanıcıyı uyaracak (Faz 2 nice-to-have).
#V5 — email ve externalId PAN içeremez
errorCode 3012 (email içinde kart no) ve 3013 (externalId içinde kart no) — iyzico bu iki field'ı tarıyor. Form input'larında PAN-shaped string gönderilmemeli. externalId formatı: wc_<user_id> güvenli.
#V6 — Card storage'a özgü errorCode'lar
| Kod | Anlamı | Workaround | |——-|———————————————————|————————————————————-| | 3001 | cardUserKey zorunlu | Vault tablosunda boşsa standalone-add varyantına düş | | 3002 | cardToken zorunlu | DELETE'te cardToken parametresi unutulmuş | | 3005 | cardUserKey sistemde tanımlı değil | Vault row stale → soft-delete edip kullanıcıdan yeniden ekleme iste | | 3006 | cardToken doğrulanamadı (silinmiş/expired) | Vault row'u purge et, charge'ı yeni kartla retry | | 3012 | email PAN benzeri içerik | Buyer email validation | | 3013 | externalId PAN benzeri içerik | externalId üretiminde wc_<user_id> formatı | | 5111 | Saklı kart ödemesinde cardUserKey veya cardToken eksik | paymentCard payload sanity-check |
7.5. PCI-DSS scope notu
/cardstorage/card (standalone) ve /payment/3dsecure/initialize (charge) endpoint'lerine raw PAN gönderiyoruz → PCI-DSS SAQ A-EP scope. Mevcut checkout-form akışımız (raw PAN bizde değil) SAQ A idi.
| Scope | PAN bizim sayfada? | Yıllık yük | |———-|———————|—————————————————| | SAQ A | Hayır | Self-assessment (form) | | SAQ A-EP | Evet (transit) | Self-assessment + ASV scan (~$300/yıl) + CSP/SRI |
Vault feature'ı kabul edildiğinde docs/security/pci-dss.md (yeni) altına SAQ A-EP checklist'i, ASV vendor seçimi, CSP policy'si yazılacak.
7.6. Sandbox testi açık sorular
registerCard=1 + 3DS failsenaryosunda iyzico kartı yine de saklıyor mu, yoksa sadece success'te mi? (muhtemelen sadece success)- Aynı
email+ farklıexternalIdile iki kez kart kayıt → tekcardUserKeymi iki ayrı mı? (quirk #V1 doğrulaması) /payment/authsaklı kartla NON3D sandbox'ta gerçekten geçiyor mu, yoksa errorCode5096"3DS zorunlu" mu dönüyor?cardAliasçok uzun (255+ char) gönderilince hangi errorCode? (sınır testi)- CheckoutForm retrieve detail response'unda kullanıcı hosted form'da "kartı kaydet" işaretlediğinde
cardUserKey/cardTokenfield'ları gerçekten görünüyor mu? (quirk #V3)
7.7. Kaynaklar (clean-room — kod kopyalanmadı)
- Card Storage | EN — birincil spec
- Kart Saklama | TR — TR mirror
- Init 3DS | EN —
registerCard:1flag - Error Codes | EN — 3001-3013, 5111
- iyzipay-dotnet referans deposu — sadece spec keşfi için tarandı
8. Değişiklik geçmişi
| Tarih | Sürüm | Değişiklik | Senkron eden | |————|———–|——————————————————————————-|————–| | 2026-05-05 | IYZWSv2 | §7 başlığı "uygulandı" — F4-iyzico vault E2E sandbox doğrulaması; #V0 hosted form kısıtı dokümante | Claude+Cenk | | 2026-05-05 | IYZWSv2 | §7 Card Storage / Vault spec araştırması — Hafta 15-16 sprint'i için temel | Claude+Cenk | | 2026-05-04 | IYZWSv2 | İlk doldurma — Hafta 14 maratonu sonrası tüm quirk'ler dokümante | Claude+Cenk | | 2026-05-04 | IYZWSv2 | Migration 1.4.0 → payment_transaction_id persist; refund DB-first | Claude+Cenk | | 2026-05-03 | IYZWSv2 | RefundService::bootstrap encrypted credentials decrypt fix | Claude+Cenk | | 2026-04-29 | IYZWSv2 | İlk iyzico adapter (charge + callback + refund) — Hafta 7 | Claude+Cenk |