.NET Core ile İyzico Entegrasyonu ve Webhook Akışı
Bu yazı, klasik “Ödeme başlat → Callback → Sipariş oluştur” akışını tekrar etmiyor. Bu kısım zaten dokümantasyonda var.
Sorunlar genelde başka yerde çıkıyor.
Kullanıcı ödeme sırasında sayfayı kapatıyor.
Geri tuşuna basıyor.
Aynı ödemeyi ikinci kez deniyor.
Bazen callback gelmiyor, bazen geç geliyor.
Bu durumlar nadir değil ve doğrudan sipariş bütünlüğünü etkiliyor.
Burada odak noktası; ideal senaryo değil, bu durumlar yaşandığında .NET Core tarafında ödeme ve sipariş akışının nasıl kontrol altında tutulabileceği.
Ödeme Başlatma Akışı
Ödeme başlatılmadan önce sipariş sistem tarafında oluşturulur ve Waiting durumunda Database’e kaydedilir. Bu aşamada sipariş henüz ödenmiş değildir; yalnızca ödeme sürecini başlatmak için kalıcı bir kayıt oluşturulur.
Aşağıda siparişin oluşturulduğu kritik kısım yer almaktadır:
var order = new Order
{
OrderNumber = await GenerateOrderNumberAsync(cancellationToken),
CartId = cart.Id,
ConversationId = Guid.NewGuid().ToString("N"),
Status = OrderStatus.Waiting
};
// order items, addresses, billing info ...
// ...
await _dbContext.Orders.AddAsync(order, cancellationToken);
await _dbContext.SaveChangesAsync(cancellationToken);Sipariş oluşturulduktan sonra ödeme süreci başlatılır. Dokümantasyonda yer alan temel ödeme başlatma işlemi aşağıdaki gibidir:
public async Task<CheckoutFormInitialize> CreatePaymentAsync(Order order)
{
var request = new CreateCheckoutFormInitializeRequest
{
Locale = Locale.TR.ToString(),
ForceThreeDS = 1,
ConversationId = order.ConversationId,
BasketId = order.CartId.ToString()
};
return await CheckoutFormInitialize.Create(request, options);
}Bu aşamada kritik bir ayrım vardır.conversationId, İyzico ile yapılan ödeme iletişiminin referansıdır; uygulama tarafındaki sipariş kimliği değildir. Bu değer yalnızca ödeme sürecini takip etmek için kullanılır.
OrderId ise uygulamanın kendi domain’ine aittir ve ödeme sağlayıcısından bağımsızdır. Bu iki alanın birebir ilişkilendirilmesi, siparişin yanlış noktada ve yanlış zamanda oluşturulmasına yol açar.
Bu nedenle OrderId ≠ ConversationId.
Callback Geldiğinde Gerçekte Ne Oluyor?
Ödeme tamamlandıktan sonra kullanıcı callback URL’ine yönlendirilir. Bu yönlendirme tamamen kullanıcı tarayıcısı üzerinden gerçekleşir ve ödeme sonucunun kesin bir göstergesi değildir. Kullanıcı bu ekrana hiç ulaşmayabilir; sekmeyi kapatabilir, geri tuşuna basabilir ya da bağlantı kopabilir.
Bu nedenle callback, ödeme sonucunu doğrulamak için değil, kullanıcıyı bilgilendirmek için kullanılmalıdır.
Callback aşamasında genellikle token ve conversationId beklenir ve birçok entegrasyon siparişi bu noktada oluşturmaya çalışır. Ancak callback isteğinde conversationId gelmez. Buna rağmen sipariş oluşturulmaya çalışıldığında, ödeme başarılı olsa bile sistem tarafında karşılığı olmayan işlemler oluşur. Sorun ödemenin kendisi değil, siparişin yanlış noktada oluşturulmasıdır.
Bu yüzden callback, sipariş oluşturma noktası değildir.
Bu aşamada callback’ten gelen token ile ödeme durumu doğrudan İyzico üzerinden tekrar sorgulanır. Callback isteğine güvenmek yerine, ödeme sonucunun sunucu tarafında doğrulanması amaçlanır.
Aşağıdaki örnekte conversationId kullanılmadan, yalnızca token üzerinden ödeme durumu kontrol edilir. Ödeme başarılıysa sepet temizlenir ve kullanıcı başarı sayfasına yönlendirilir; aksi durumda hata sayfasına yönlendirme yapılır.
public async Task<string> Handle(PaymentResultCommand request, CancellationToken cancellationToken)
{
var formRequest = new RetrieveCheckoutFormRequest
{
Token = request.Token
};
var options = new Options
{
ApiKey = _configuration["Iyzico:ApiKey"],
SecretKey = _configuration["Iyzico:SecretKey"],
BaseUrl = _configuration["Iyzico:BaseUrl"]
};
var checkoutForm = await CheckoutForm.Retrieve(formRequest, options);
if (checkoutForm.PaymentStatus == "SUCCESS")
{
await _dbContext.Carts
.Where(c => c.Id == Guid.Parse(checkoutForm.BasketId))
.ExecuteDeleteAsync(cancellationToken);
return _configuration["Iyzico:SuccessUrl"];
}
return _configuration["Iyzico:FailUrl"];
}Webhook Üzerinden Sipariş Güncelleme
Webhook endpoint’ine gelen isteği tek bir yerde işlemek yerine, akışı bilinçli olarak parçalıyoruz. Amaç; production’da sorunsuz çalışan, okunabilir ve genişletilebilir bir yapı kurmak.
Bu akışta sırasıyla şunlar yapılır:
- İsteğin gerçekten İyzico’dan gelip gelmediği doğrulanır
- Payload ortak bir context haline getirilir
- Event ve status bilgisine göre dispatcher devreye girer
- Sipariş durumu güncellenir (
Waiting → Received / Failed)
Controller bu sürecin detaylarını bilmez. Sadece isteği alır ve akışı başlatır.
⚠️ Buradan sonra seviye biraz yükseliyor, Senior modu açık. Kodlar production’dan 😂
IyzicoSignatureValidator
Webhook tarafında ilk durak burasıdır. İmza doğrulanmadan dispatcher’a veya sipariş güncelleme akışına geçilmez. Bu class’ın tek sorumluluğu vardır: Gelen isteğin gerçekten İyzico’dan gelip gelmediğini doğrulamak.
public interface IIyzicoSignatureValidator
{
bool Validate(IyzicoWebhookRequest request, string signature);
}public class IyzicoSignatureValidator
{
private readonly IConfiguration _configuration;
public IyzicoSignatureValidator(IConfiguration configuration)
{
_configuration = configuration;
}
public void Validate(JsonElement payload, string signature)
{
if (string.IsNullOrEmpty(signature))
throw new SecurityException(
"Required request authentication data is missing."
);
var secretKey = _configuration["Iyzico:SecretKey"];
var eventType = payload.GetProperty("iyziEventType").GetString();
var paymentId = payload.GetProperty("iyziPaymentId").GetInt64().ToString();
var conversationId = payload.GetProperty("paymentConversationId").GetString();
var status = payload.GetProperty("status").GetString();
var token = payload.TryGetProperty("token", out var outputToken)
? outputToken.GetString()
: string.Empty; // optional
var raw =
secretKey +
eventType +
paymentId +
token +
conversationId +
status;
using var hmac = new HMACSHA256(
Encoding.UTF8.GetBytes(secretKey)
);
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(raw));
var computed = Convert.ToHexString(hash).ToLowerInvariant();
if (!computed.Equals(signature, StringComparison.OrdinalIgnoreCase))
throw new SecurityException(
"Request signature could not be verified."
);
}
}IyzicoWebhookContext
Doğrulanan webhook isteği, dispatcher ve handler’lar tarafından doğrudan kullanılmaz. Bunun yerine, ihtiyaç duyulan alanları içeren tek bir context oluşturulur.
public class IyzicoWebhookContext
{
public string EventType { get; init; }
public string Status { get; init; }
public string ConversationId { get; init; }
public bool IsValid =>
!string.IsNullOrEmpty(EventType) &&
!string.IsNullOrEmpty(Status) &&
!string.IsNullOrEmpty(ConversationId);
public static IyzicoWebhookContext From(JsonElement payload)
=> new()
{
EventType = payload.GetProperty("iyziEventType").GetString(),
Status = payload.GetProperty("status").GetString(),
ConversationId = payload.GetProperty("paymentConversationId").GetString()
};
}IyzicoWebhookDispatcher
Webhook tarafında bütün kararlar bu sınıfta verilir. Controller’dan sonra girilen tek kapı burasıdır.
İlk yapılan şey, isteğin gerçekten İyzico’dan gelip gelmediğini doğrulamaktır. İmza doğrulanmadan hiçbir akış ilerlemez.
public class IyzicoWebhookDispatcher
{
public IyzicoWebhookDispatcher()
{
_dbContext = dbContext;
....
}
public async Task DispatchAsync(JsonElement payload, CancellationToken cancellationToken)
{
_signatureValidator.Validate(payload, _iyzicoAccessor.Signature);
var context = IyzicoWebhookContext.From(payload);
if (!context.IsValid)
return;
if (context.EventType != IyzicoWebhookEvents.CheckoutFormAuth)
return;
switch (context.Status)
{
case IyzicoWebhookStatuses.Success:
await HandleSuccessAsync(context, cancellationToken);
break;
case IyzicoWebhookStatuses.Failure:
await HandleFailureAsync(context, cancellationToken);
break;
}
}
private async Task HandleSuccessAsync(IyzicoWebhookContext context,
CancellationToken cancellationToken)
{
var order = await _dbContext.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o =>
o.ConversationId == context.ConversationId,
cancellationToken);
if (order is null)
return;
if (order.Status != OrderStatus.Waiting &&
order.Status != OrderStatus.Failed)
return;
order.Status = OrderStatus.Received;
await _dbContext.SaveChangesAsync(cancellationToken);
await _mediator.Publish(
new SendEmailNotification(
"OrderReceived",
order.Email,
template),
cancellationToken);
}
private async Task HandleFailureAsync(IyzicoWebhookContext context,
CancellationToken cancellationToken)
{
var order = await _dbContext.Orders
.FirstOrDefaultAsync(o =>
o.ConversationId == context.ConversationId,
cancellationToken);
if (order is null)
return;
if (order.Status != OrderStatus.Waiting)
return;
order.Status = OrderStatus.Failed;
await _dbContext.SaveChangesAsync(cancellationToken);
}
}İlk yapılan iş, webhook’un gerçekten İyzico’dan geldiğini doğrulamaktır.
_signatureValidator.Validate(payload, _iyzicoAccessor.Signature);Bu satır geçmezse hiçbir şeyin önemi yoktur. Order bakılmaz, event bakılmaz, status bakılmaz.
Get Fatih Erol’s stories in your inbox
Join Medium for free to get updates from this writer.
İmza geçtikten sonra raw JSON, domain’in anlayacağı hale çevrilir.
var context = IyzicoWebhookContext.From(payload);Context geçerli değilse akış burada sessizce kapanır.
if (!context.IsValid)
return;Webhook tarafında exception fırlatılmaz. Gürültü yok, retry tetikleme yok.
Bu dispatcher her webhook’u işlemez. Sadece ilgilenilen event için devam eder.
if (context.EventType != IyzicoWebhookEvents.CheckoutFormAuth)
return;Böylece farklı iyzico event’leri geldiğinde bu class büyümez.
Event uygunsa bu kez ödeme sonucuna bakılır.
switch (context.Status)
{
case IyzicoWebhookStatuses.Success:
await HandleSuccessAsync(context, cancellationToken);
break;
case IyzicoWebhookStatuses.Failure:
await HandleFailureAsync(context, cancellationToken);
break;
}Burada dispatcher karar verir, iş yapmaz.
Başarılı ödeme senaryosunda ilk iş siparişi bulmaktır.
var order = await _dbContext.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o =>
o.ConversationId == context.ConversationId, cancellationToken);Eşleşme yalnızca ConversationId üzerinden yapılır. Callback’te gelen başka hiçbir şey burada kullanılmaz.
Sipariş bulunamazsa akış kesilir.
if (order is null)
return;Webhook gelmiş olabilir ama sistemde karşılığı olmayabilir. Bu normal bir senaryodur.
Siparişin durumu kontrol edilir.
if (order.Status != OrderStatus.Waiting &&
order.Status != OrderStatus.Failed)
return;Bu satır idempotency’nin kendisidir. Webhook 5 kere gelse de bu noktadan sonra tekrar işlenmez.
Ödeme başarılı kabul edildiğinde sipariş durumu güncellenir.
order.Status = OrderStatus.Received;
await _dbContext.SaveChangesAsync(cancellationToken);Durum güncellendikten sonra kullanıcı bilgilendirilir.
var template = _templateBuilder.Build("OrderReceived",
new
{
order.OrderNumber,
OrderDate = order.CreatedAt.ToString("dd.MM.yyyy"),
Total = order.OrderItems.Sum(i => i.Quantity * i.Price).ToCurrency()
});Mail içeriği burada hazırlanır, gönderilmez.Dispatcher burada mail göndermekle uğraşmaz. Sadece olayı yayınlar.
Ödeme başarısızsa durum güncellenir.
order.Status = OrderStatus.Failed;
await _dbContext.SaveChangesAsync(cancellationToken);Webhook Endpoint (Controller)
[HttpPost("iyzico")]
public async Task<IActionResult> ReceiveIyzicoWebhook(
[FromBody] JsonElement payload,
CancellationToken cancellationToken)
{
await _iyzicoDispatcher.DispatchAsync(payload, cancellationToken);
return Ok();
}Bu endpoint’in tek bir görevi vardır: Webhook isteğini alır ve hiç yorumlamadan dispatcher’a devreder.
Bu Yapı Ne Kazandırıyor?
Bu akış ile birlikte:
- Sipariş ödeme öncesinde oluşturulur
- Callback, sipariş oluşturan bir nokta olmaktan çıkar
- Webhook, tek doğrulama kaynağı haline gelir
- Sipariş durumu yalnızca backend tarafından güncellenir
Webhook’un:
- geç gelmesi
- birden fazla gelmesi
- hiç gelmemesi
gibi senaryolar sipariş bütünlüğünü bozmaz.
Özetle
Bu yapı ile birlikte ödeme akışı daha öngörülebilir hale gelir.
Siparişin ne zaman oluşturulduğu, ne zaman güncellendiği ve hangi aşamada hangi kararın verildiği nettir.
Kullanıcının tarayıcıyı kapatması, geri tuşuna basması ya da ağ problemleri yaşanması sipariş bütünlüğünü etkilemez.
Çünkü sipariş durumu, istemci tarafındaki varsayımlara değil; sunucu tarafında doğrulanan verilere dayanır.
Callback yalnızca kullanıcıyı bilgilendirmek için kullanılır.
Ödeme sonucunun kesinleştiği yer ise webhook ve onun arkasındaki kontrol mekanizmalarıdır.
Bu yaklaşım, ödeme entegrasyonlarında sıkça karşılaşılan belirsizlikleri ortadan kaldırır ve sistemin her koşulda tutarlı kalmasını sağlar.



