1. 01Ana Sayfa
  2. 02Hakkımda
  3. 03Hizmetler
  4. 04Projeler
  5. 05Blog
  6. 06İletişim
TREN
Ana Sayfa/Blog/E-Ticaret
E-TicaretHaz 2026·10 dk okuma

Medusa V2: Ödeme entegrasyonu (Stripe/Iyzico/PayTR + payment session + webhook)

Bu yazıda Medusa v2 tabanlı bir storefront'ta ödeme mimarisini—payment collection, payment session, sağlayıcı seçimi, custom provider yazımı ve webhook ile sipariş tamamlamayı—uçtan uca ele alıyorum.

Bu yazıda Medusa v2 tabanlı bir storefront’ta ödeme mimarisini—payment collection, payment session, sağlayıcı seçimi, custom provider yazımı ve webhook ile sipariş tamamlamayı—uçtan uca ele alıyorum.

Medusa V2 ile Ödeme Entegrasyonu Nasıl Kurulur?

Önceki iki yazıda headless mimariyi kurmuş ve checkout akışını tasarlamıştık. İkisinde de tekrar tekrar geçen ama hiç açılmayan bir kavram vardı: payment session. Checkout’un “ödeme” adımına gelen kullanıcı için artık asıl soru şu — sepet hazır, akış net, peki gerçek bir ödeme sağlayıcısına nasıl bağlanıyoruz?

Medusa v2’de ödeme, frontend’in tek başına çözebileceği bir konu değil. Tutar, vergi, indirim ve para birimi hesapları backend’de tutulduğu gibi, ödeme oturumunun durumu da backend’de tutulur. Storefront yalnızca sağlayıcı seçimini ve sağlayıcının istediği arayüz adımını (kart girişi, 3D Secure yönlendirmesi) yönetir. Bu ayrım, çift çekim (double-charge) gibi en pahalı hataları en başından engellediği için kritik.

Ödeme Mimarisini Anlamak

Medusa’da ödemenin üç temel nesnesi var: payment collection, payment session ve payment provider. Bir payment collection, tek bir kaynağın (genelde sepetin) tüm ödeme operasyonlarını taşır. Bu collection içinde bir veya daha fazla payment session bulunur; her session, belirli bir sağlayıcı tarafından yetkilendirilecek (authorize) bir tutarı temsil eder. Provider ise Stripe, Iyzico ya da kendi yazdığın bir entegrasyon olabilir.

Akış kavramsal olarak şöyle ilerler:

Sepet (cart)
  -> Payment Collection oluştur
     -> Payment Session başlat (provider seçilince)
        -> Sağlayıcıda ödeme arayüzü / yönlendirme
           -> authorize (senkron) veya webhook (asenkron)
              -> Cart Complete -> Order

Buradaki en önemli nokta: storefront bu nesneleri tek tek elle yönetmez. JS SDK, payment collection oluşturma ve session başlatma adımlarını tek bir initiatePaymentSession çağrısında birleştirir. Senin işin doğru sağlayıcıyı seçmek ve sağlayıcının döndürdüğü veriye göre kullanıcıya uygun adımı göstermek.

Stripe Entegrasyonu (Resmi Provider)

Stripe, Medusa’nın kutudan çıkan resmi provider’ıyla geliyor. Backend tarafında yapılması gereken tek şey, Payment Module’ün altına Stripe sağlayıcısını eklemek ve anahtarları ortam değişkenlerinden okumak.

// medusa-config.ts
import { defineConfig } from "@medusajs/framework/utils"

export default defineConfig({
  modules: [
    {
      resolve: "@medusajs/medusa/payment",
      options: {
        providers: [
          {
            resolve: "@medusajs/medusa/payment-stripe",
            id: "stripe",
            options: {
              apiKey: process.env.STRIPE_API_KEY,
              webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
            },
          },
        ],
      },
    },
  ],
})

Provider’ı tanımladıktan sonra onu bölgeye (region) de eklemen gerekir; çünkü hangi bölgede hangi ödeme yönteminin sunulacağı region üzerinden belirlenir. Bu, çok bölgeli bir kurulumda Türkiye’ye Iyzico/PayTR, Avrupa’ya Stripe gösterebilmenin de temelini oluşturur. Anahtarların asla repo’ya girmediğinden emin ol; STRIPE_API_KEY ve STRIPE_WEBHOOK_SECRET yalnızca ortam değişkeni olarak yaşamalı.

Storefront Tarafında Ödeme Akışı

Storefront önce o bölgede kullanılabilir sağlayıcıları listeler, kullanıcı birini seçince de o sağlayıcı için bir ödeme oturumu başlatır. Bu iki işlemi tek bir servis dosyasında topluyoruz ki bileşenler API ayrıntısını bilmesin.

// lib/payment.ts
import { sdk } from "@/lib/medusa"
import { HttpTypes } from "@medusajs/types"

// Bölgeye göre kullanılabilir ödeme sağlayıcılarını getir
export async function listPaymentProviders(regionId: string) {
  const { payment_providers } = await sdk.store.payment.listPaymentProviders({
    region_id: regionId,
  })
  return payment_providers
}

// Sağlayıcı seçilince payment collection + session oluştur
export async function initPayment(
  cart: HttpTypes.StoreCart,
  providerId: string
) {
  return sdk.store.payment.initiatePaymentSession(cart, {
    provider_id: providerId,
  })
}

Burada provider_id, listeden dönen sağlayıcının kimliğidir ve pp_{provider}_{id} biçimindedir — örneğin pp_stripe_stripe. Bir uyarı: tutarı 0 olan bir sepet için Stripe oturumu başlatmaya çalışmak hata fırlatır, çünkü Stripe sıfır tutarlı bir oturum oluşturamaz. Bu yüzden session’ı yalnızca sepet toplamı sıfırdan büyükken başlat; ücretsiz/sıfır tutarlı durumlar için Medusa’nın varsayılan Manual sağlayıcısını kullanmak daha doğru.

Sağlayıcıya Göre Ödeme Arayüzü

Oturum başladıktan sonra, kullanıcıya gösterilecek arayüz seçilen sağlayıcıya göre değişir. Stripe’ta kart bilgisi alanı gösterilirken, yönlendirme tabanlı bir sağlayıcıda kullanıcı sağlayıcının ödeme sayfasına gönderilir. Bu dallanmayı tek bir yerde toplamak, ileride yeni sağlayıcı eklemeyi kolaylaştırır.

// components/checkout/payment-ui.tsx
import { HttpTypes } from "@medusajs/types"

export function getPaymentUi(cart: HttpTypes.StoreCart) {
  const session = cart.payment_collection?.payment_sessions?.[0]
  if (!session) return null

  // Stripe: kart girişi arayüzünü göster
  if (session.provider_id.startsWith("pp_stripe_")) {
    return (
      <StripePayment
        clientSecret={session.data.client_secret as string}
      />
    )
  }

  // Iyzico / PayTR: sağlayıcının ödeme sayfasına yönlendir
  return <RedirectPayment session={session} />
}

Stripe için @stripe/react-stripe-js ile kart elemanlarını render eder, kullanıcı kart bilgisini girer ve ödeme client_secret üzerinden onaylanır. Yönlendirme tabanlı sağlayıcılarda ise session.data içinde dönen ödeme sayfası URL’ine kullanıcıyı gönderirsin. Hangi durumda olursa olsun, sipariş yalnızca sepet tamamlandığında oluşur — kart arayüzünü göstermek tek başına ödemeyi bitirmez.

Cart Completion → Sipariş

Kullanıcı ödeme adımını tamamladığında sepeti “complete” ederiz. Bu çağrı, payment session’ı sağlayıcıda yetkilendirir ve başarılıysa sepeti bir siparişe dönüştürür. Dönen yanıtın tipini kontrol etmek şart: sonuç ya bir sipariş ya da hâlâ tamamlanamamış bir sepettir.

// lib/order.ts
import { sdk } from "@/lib/medusa"

export async function placeOrder(cartId: string) {
  const res = await sdk.store.cart.complete(cartId)

  if (res.type === "order") {
    // Sipariş oluştu: cart_id'yi temizle, onay sayfasına yönlendir
    return { orderId: res.order.id }
  }

  // type === "cart" → tamamlanamadı, kullanıcıya anlaşılır mesaj göster
  throw new Error(res.error?.message ?? "Sipariş tamamlanamadı, lütfen tekrar deneyin.")
}

İki nokta üzerinde durmak gerekiyor. Birincisi idempotency: kullanıcı “Siparişi Tamamla” butonuna iki kez basarsa ya da ağ yeniden denemesi olursa, aynı sepet için ikinci bir complete çağrısı yeni bir ödeme tetiklememeli. Butonu çağrı süresince disable etmek ve cart_id temizlenene kadar yönlendirmeyi beklemek bunun en basit savunmasıdır. İkincisi, başarılı dönüşten sonra cart_id’yi localStorage/cookie’den mutlaka kaldırmak; aksi halde kullanıcı tamamlanmış bir sepetle dönüp kafa karıştırıcı bir durumla karşılaşır.

Custom Payment Provider Yazmak (Iyzico / PayTR)

Stripe’ın resmi provider’ı var, ama Türkiye pazarının iki yaygın sağlayıcısı olan Iyzico ve PayTR için hazır bir Medusa provider’ı yok. Bu durumda AbstractPaymentProvider’ı genişletip kendi sağlayıcını yazıyorsun. Çoğu Medusa içeriğinin atladığı, ama gerçek TR projelerinde kaçınılmaz olan kısım burası.

Bir provider, ödeme yaşam döngüsünün tüm aşamalarını metot olarak implemente eder: başlatma, yetkilendirme, çekim, iade, iptal, durum sorgulama ve webhook normalizasyonu.

// src/modules/iyzico/service.ts
import { AbstractPaymentProvider } from "@medusajs/framework/utils"
import {
  InitiatePaymentInput, InitiatePaymentOutput,
  AuthorizePaymentInput, AuthorizePaymentOutput,
  CapturePaymentInput, CapturePaymentOutput,
  RefundPaymentInput, RefundPaymentOutput,
  CancelPaymentInput, CancelPaymentOutput,
  GetPaymentStatusInput, GetPaymentStatusOutput,
  ProviderWebhookPayload, WebhookActionResult,
} from "@medusajs/framework/types"

class IyzicoProviderService extends AbstractPaymentProvider {
  static identifier = "iyzico"

  // Ödeme oturumu başlat: Iyzico'da bir ödeme isteği oluştur,
  // dönen token/URL'i session data'sına yaz.
  async initiatePayment(
    input: InitiatePaymentInput
  ): Promise<InitiatePaymentOutput> {
    // const form = await this.client.createCheckoutForm({ amount: input.amount, ... })
    return {
      id: "iyzico_session_id",
      data: {
        // token, paymentPageUrl ...
      },
    }
  }

  async authorizePayment(
    input: AuthorizePaymentInput
  ): Promise<AuthorizePaymentOutput> {
    return { status: "authorized", data: input.data }
  }

  async capturePayment(
    input: CapturePaymentInput
  ): Promise<CapturePaymentOutput> {
    return { data: input.data }
  }

  async refundPayment(
    input: RefundPaymentInput
  ): Promise<RefundPaymentOutput> {
    return { data: input.data }
  }

  async cancelPayment(
    input: CancelPaymentInput
  ): Promise<CancelPaymentOutput> {
    return { data: input.data }
  }

  async getPaymentStatus(
    input: GetPaymentStatusInput
  ): Promise<GetPaymentStatusOutput> {
    return { status: "authorized" }
  }

  // Iyzico/PayTR callback'ini normalize et → Medusa'ya ne yapacağını söyle
  async getWebhookActionAndData(
    payload: ProviderWebhookPayload["payload"]
  ): Promise<WebhookActionResult> {
    // 1. İmzayı/hash'i doğrula
    // 2. Sağlayıcı durumunu oku (success / fail)
    return {
      action: "authorized",
      data: { session_id: "...", amount: 0 },
    }
  }
}

export default IyzicoProviderService

Provider’ı bir modül olarak tanımlayıp Payment Module’e bağlarsın:

// src/modules/iyzico/index.ts
import { ModuleProvider, Modules } from "@medusajs/framework/utils"
import IyzicoProviderService from "./service"

export default ModuleProvider(Modules.PAYMENT, {
  services: [IyzicoProviderService],
})

Ardından medusa-config.ts’teki Payment Module’ün providers dizisine, tıpkı Stripe gibi eklersin:

{
  resolve: "./src/modules/iyzico",
  id: "iyzico",
  options: {
    apiKey: process.env.IYZICO_API_KEY,
    secretKey: process.env.IYZICO_SECRET_KEY,
  },
}

initiatePayment içinde sağlayıcının client’ını constructor’da kurup üçüncü parti API’sini çağırırsın; options parametresi de yine constructor üzerinden gelir. Bu iskelet, gerçek HTTP çağrılarını eklediğinde tam çalışan bir TR ödeme sağlayıcısına dönüşür.

Webhook’lar ve Asenkron Onay

Stripe’ta kart onayı çoğu zaman senkron tamamlanır; ama Iyzico ve PayTR gibi 3D Secure ve yönlendirme tabanlı sağlayıcılarda ödeme asenkron biter. Kullanıcı sağlayıcının sayfasında işlemi tamamlar, sağlayıcı sonucu sana bir webhook ile bildirir.

Medusa bunun için kutudan çıkan bir webhook route’u sunar: /hooks/payment/{identifier}_{provider}. Örneğin Iyzico sağlayıcın için bu adres /hooks/payment/iyzico_iyzico olur. Bu route, Payment Module’ün getWebhookActionAndData metodunu çağırır; o da olayı ilgili provider’a iletir. Metot authorized veya captured döndürdüğünde Medusa ilgili payment session’ı bu duruma getirir ve eğer sepet henüz tamamlanmamışsa sepeti otomatik olarak tamamlar.

Pratikteki tipik akış şöyle olur:

Kullanıcı ödeme sayfasına yönlendirilir
  -> İşlemi tamamlar
     -> Sağlayıcı webhook gönderir  ->  /hooks/payment/iyzico_iyzico
        -> getWebhookActionAndData "authorized" döndürür
           -> Medusa session'ı authorize eder ve cart'ı complete eder
     -> Sağlayıcı kullanıcıyı return_url'e geri yollar
        -> return sayfası sipariş durumunu kontrol eder ve onay ekranı gösterir

Burada en sık yapılan hata, siparişin tamamlanmasını yalnızca kullanıcının geri döndüğü return_url’e bağlamaktır. Kullanıcı tarayıcıyı kapatırsa o sayfa hiç çalışmaz; ama webhook yine de gelir. Bu yüzden gerçeğin kaynağı webhook olmalı, return sayfası yalnızca doğrulama ve görsel onaydan sorumlu olmalı. Webhook imzasını doğrulamadan hiçbir durumu güvenilir kabul etme.

Sonuç

Medusa v2’de ödeme entegrasyonunun özü, “kart bilgisini nasıl alırım” değil, ödeme durumunun nerede tutulduğunu doğru kurmaktır. Tutar ve oturum backend’de, sağlayıcı seçimi ve arayüz adımı storefront’ta, asenkron onay ise webhook’ta yaşar. Stripe için resmi provider işini görür; Türkiye pazarında Iyzico ve PayTR için AbstractPaymentProvider’ı genişleterek kendi sağlayıcını yazarsın. Bu üçlü—resmi provider, custom provider ve webhook—doğru kurulduğunda ödeme, e-ticaretin en kırılgan adımı olmaktan çıkıp güvenle ölçülebilen bir akışa dönüşür.

Pratikte Dikkat Edilmesi Gereken Detaylar

E-ticaret altyapısında ödeme tarafının en pahalı hatası, sepet toplamı ile ödemeye gönderilen tutarın farklı kaynaklardan beslenmesidir. Kullanıcının gördüğü tutar, payment session’a giden tutar ve siparişte yazılan tutar aynı veri modelinden gelmeli. Para birimi, indirim ve vergi hesapları region üzerinden tek bir yerde çözülmezse, kullanıcı güvenini en kötü anda—ödeme ekranında—kaybeder.

İkinci kritik nokta güvenliktir. Sağlayıcı anahtarları yalnızca backend’de ve ortam değişkeni olarak yaşamalı; storefront’a yalnızca publishable/public anahtarlar inmeli. Webhook’larda imza doğrulaması atlanırsa, sahte bir “ödendi” isteği siparişi tetikleyebilir. Bu yüzden her webhook, işlenmeden önce mutlaka doğrulanmalı.

Test Senaryoları

Canlıya çıkmadan önce yalnızca başarılı akışı değil, başarısız yolları da test et. Ödeme yarıda iptal edilirse kullanıcı ne görüyor? 3D Secure ekranında zaman aşımı olursa sepet ne durumda kalıyor? Webhook geldi ama kullanıcı geri dönmediyse sipariş yine de tamamlanıyor mu? Aynı sepet için complete çağrısı iki kez gelirse ikinci çağrı çift sipariş yaratıyor mu? Sağlayıcı tarafında ödeme başarılı ama Medusa tarafında session güncellenememişse durum nasıl onarılıyor? Bu soruların hepsi test planında olmalı.

Metrikler

Ödeme tarafının başarısını yalnızca sipariş sayısıyla ölçmek yetersizdir. Ödeme adımına gelen kullanıcıların tamamlama oranı, sağlayıcı bazında başarı/başarısızlık oranı, 3D Secure terk oranı, webhook gecikme süresi ve çift çekim/iade vakalarının sıklığı birlikte izlenmeli. Özellikle sağlayıcı bazında hata oranı, bir entegrasyonun mu yoksa bir kullanıcı deneyimi adımının mı sorunlu olduğunu ayırt etmeni sağlar.

Uygulama Planı

Bu yaklaşımı gerçek bir projeye uygularken küçük ama uçtan uca çalışan bir çekirdekle başla. İlk adım, tek bir sağlayıcıyla (örneğin Stripe veya Manual) tam akışı—session başlatma, complete, onay sayfası—çalışır hale getirmek olmalı. İkinci adımda webhook tabanlı asenkron onayı devreye al. Üçüncü adımda ise TR sağlayıcıları için custom provider’ı ekle. Bu sırayla ilerlemek, en kritik adımı erkenden sağlamlaştırırken karmaşıklığın zamanından önce büyümesini engeller.

Ek olarak her teknik karar, bir kullanıcı ya da operasyon gerekçesine dayanmalı. Bir sağlayıcı, bir webhook ya da bir retry mekanizması yalnızca teknik olarak doğru olduğu için değil, ödemeyi daha güvenli, daha izlenebilir veya daha az terk edilen bir adım haline getirdiği için eklenmeli. Sağlam ürünler bu disiplinle büyür.

Medusa v2PaymentsStripeIyzicoNext.js
Paylaş

0 Yorum

Yorum bırak

Robot değilim reCAPTCHA