Çok kanallı satışta aşırı satışı (oversell) önleyen şey stok sayısını daha sık göndermek değildir. Pazaryerlerine yapılan her güncelleme asenkrondur: Trendyol bir batchRequestId döndürür ve işi kuyruğa alır, N11 IN_QUEUE durumuyla bekletir, Hepsiburada aynı anda bekleyen 5 işten fazlasını kabul etmez. Yani sizin "stok 3" dediğiniz an ile pazaryerinin "stok 3" gösterdiği an arasında her zaman bir pencere vardır. Tutarlılığı sağlayan üç şey şudur: SKU başına tek yazar, monoton sürüm numarasıyla eski olayın atılması ve rezerve edilmiş stoğu satılabilir stoktan düşen bir model. Bu yazı o üçünün nasıl kurulacağını anlatıyor.
Aşağıdaki pazaryeri kısıtları 25 Eylül 2026'da resmî geliştirici dokümanlarından doğrulanmıştır. API sözleşmelerinin karşılaştırması pazaryeri entegrasyonu yazımızda; burada yalnızca senkronizasyon mimarisi var.
Üç Ayrı Stok Sayısı Tutmadan Başlamayın
Tek bir stok sütunuyla çok kanallı satış yapılmaz. En az üç sayı gerekir ve bunları birbirinden türetmek yerine ayrı ayrı saklamak gerekir:
| Sayı | Ne zaman değişir | Kim değiştirir |
|---|---|---|
| Fiziksel stok | Mal girişi, sayım, sevkiyat, fire | Depo / ERP |
| Rezerve stok | Sipariş düştüğünde artar, sevk veya iptalde azalır | Sipariş servisi |
| Satılabilir stok | Yukarıdakilerin ikisi de değiştiğinde hesaplanır | Türetilir, elle yazılmaz |
Kanala gönderilen sayı satılabilir stoktur ve formülü şudur:
satilabilir = fiziksel - rezerve - emniyet_payi
emniyet_payi tartışmalı bir kalemdir ve sabit bir sayı olmamalıdır. Anlamı şu: senkronizasyon penceresi boyunca kaç adet satılabileceğini tahmin ediyorsunuz. Günde 2 adet satılan bir ürün için 5 dakikalık pencerede emniyet payı sıfır olabilir; saatte 40 adet satılan bir ürün için aynı pencerede 3-4 adet gerekir. Emniyet payını ürünün satış hızından türetin, tüm katalog için tek bir sayı yazmayın. Tek sayı yazan sistemler yavaş satan ürünlerde gereksiz stok kilitler, hızlı satanlarda ise yine aşırı satar.
Aşırı Satış Kaçınılmazdır, Soru Onu Kaç Dakikada Kapattığınız
Pencere şu adımlardan oluşur ve hiçbiri sıfırlanamaz:
- Pazaryerinde sipariş oluşur.
- Webhook size ulaşır (ya da tarama işiniz onu bulur). Trendyol'un webhook'u başarısız isteklerde 5 dakikada bir yeniden dener; o sırada siz siparişten habersizsiniz.
- Stok düşer, satılabilir yeniden hesaplanır, kuyruğa iş atılır.
- Kuyruk işi kota penceresi içinde gönderilir. Trendyol'da stok/fiyat yazma kotası satıcı kademesine göre dakikada 350 ile 2.000 istek arasındadır ve bu kota o servis grubundaki tüm uçlarla paylaşılır.
- Pazaryeri işi kuyruğa alır ve işler. Sonucu görmek için Trendyol'da
batchRequestIdsorgulanır, N11'detaskIdIN_QUEUEdurumundanPROCESSED'a geçer.
Bu zincir iyi bir günde saniyeler, kötü bir günde dakikalar sürer. Kötü günü mimariye yazın: emniyet payı, bu pencerenin uzunluğu ile satış hızının çarpımıdır. Pencereyi kısaltamıyorsanız payı büyütmek zorundasınız; bu bir ayar değil, bir denklem.
SKU Başına Tek Yazar
Aşırı satışların büyük kısmı stok hesabından değil, aynı SKU'ya aynı anda yazan iki işten doğar. Tipik senaryo: sipariş webhook'u stoğu 12'den 11'e düşürür, aynı saniyede gece taraması eski bir anlık görüntüden 12 yazar. Pazaryerinde stok 12 kalır, satılmış ürün yeniden satışa açılır.
Çözüm kanal ve SKU bazında kilit almaktır. Laravel'de bunun hazır aracı Illuminate\Queue\Middleware\WithoutOverlapping'dir ve dokümantasyonun kendisi bu amaç için önerir: aynı anda yalnızca bir işin değiştirmesi gereken bir kaynak varsa ShouldBeUnique yerine bu ara katmanı kullanın. Kilidin takılı kalmasına karşı expireAfter() şarttır; iş zaman aşımına uğrayıp kilidi bırakmazsa o SKU sonsuza kadar güncellenemez hâle gelir.
<?php
use Illuminate\Queue\Middleware\WithoutOverlapping;
final class KanalStokGonder implements ShouldQueue
{
public function __construct(
public string $kanal, // 'trendyol' | 'hepsiburada' | 'n11'
public string $sku,
public int $surum, // monoton artan; kaynak sistemde uretilir
) {}
public function middleware(): array
{
return [
(new WithoutOverlapping("stok:{$this->kanal}:{$this->sku}"))
->releaseAfter(5) // cakisirsa 5 sn sonra kuyruga geri koy
->expireAfter(120), // kilit en fazla 120 sn yasar
];
}
}
Geç Gelen Eski Değer: Sürüm Numarası Olmadan Çözülmez
Kilit eşzamanlılığı çözer, sırayı çözmez. İki iş sırayla çalışabilir ama yanlış sırayla: 11 yazan iş kuyrukta takılır, 12 yazan iş önce gider, sonra 11 gider ve sonuç doğru görünür. Ters sırada olursa sonuç yanlıştır ve bunu fark etmezsiniz çünkü her iki istek de 200 döner.
Kural şu: her stok değişikliğine kaynak sistemde monoton artan bir sürüm numarası verin ve kanala göndermeden önce o kanala en son hangi sürümün gönderildiğine bakın. Eski sürümü sessizce atın.
<?php
// Kanal basina "golge durum" tablosu: o kanala EN SON ne gonderdik?
// Hem sira kontrolu hem de delta gonderimi bu tablodan cikar.
//
// kanal_stok_durumu
// kanal, sku, son_surum, son_adet, son_satis_fiyati, son_liste_fiyati,
// son_gonderim_at, son_batch_id
public function handle(KanalIstemcisi $istemci): void
{
$durum = KanalStokDurumu::lockForUpdate()
->firstOrNew(['kanal' => $this->kanal, 'sku' => $this->sku]);
// 1) Sira kontrolu: gec gelen eski olayi AT.
if ($durum->son_surum !== null && $this->surum <= $durum->son_surum) {
return;
}
$hedef = SatilabilirStok::hesapla($this->sku, $this->kanal);
// 2) Delta kontrolu: degismediyse hic gonderme.
// Trendyol ayni istegin 15 dakika icinde tekrarlanmamasini istiyor.
if ($durum->son_adet === $hedef->adet
&& $durum->son_satis_fiyati === $hedef->satisFiyatiKurus) {
$durum->son_surum = $this->surum; // surumu yine de ilerlet
$durum->save();
return;
}
$batchId = $istemci->stokFiyatGonder($this->sku, $hedef);
$durum->fill([
'son_surum' => $this->surum,
'son_adet' => $hedef->adet,
'son_satis_fiyati' => $hedef->satisFiyatiKurus,
'son_liste_fiyati' => $hedef->listeFiyatiKurus,
'son_gonderim_at' => now(),
'son_batch_id' => $batchId,
])->save();
}
Bu tablo aynı anda üç problemi çözer: sıra kontrolü, delta gönderimi ve asenkron sonuç takibi. Delta gönderimi isteğe bağlı bir optimizasyon değildir; Trendyol'un dokümanı aynı isteğin 15 dakika içinde tekrar gönderilmemesini ve yalnızca değişen stok/fiyatın iletilmesini açıkça istiyor.
Idempotency: Aynı Olayı İki Kez İşlemek
Webhook'lar en az bir kez teslim edilir, bazen birden fazla kez. Trendyol başarısız istekleri 5 dakikada bir tekrarlıyor; yanıtınız 200 dönmeden önce zaman aşımına uğrarsa aynı sipariş size iki kez ulaşır ve stoğu iki kez düşersiniz.
Webhook alıcısının tek işi olmalıdır: olayı tekilleştirerek kaydetmek ve hemen 200 dönmek. İşleme kuyrukta yapılır.
<?php
// Webhook alicisi: dogrula, tekillestir, kaydet, 200 don. Is mantigi YOK.
public function trendyolWebhook(Request $r): Response
{
$olayAnahtari = hash('sha256', implode('|', [
'trendyol',
$r->input('shipmentPackageId'), // paket kimligi
$r->input('status'), // statu gecisi
$r->input('packageModificationDate') ?? '',
]));
$olay = KanalOlayi::firstOrCreate(
['anahtar' => $olayAnahtari],
['kanal' => 'trendyol', 'govde' => $r->getContent(), 'durum' => 'yeni']
);
if ($olay->wasRecentlyCreated) {
KanalOlayiniIsle::dispatch($olay->id);
}
return response()->noContent(); // 204, her durumda basarili
}
İki ayrıntı önemli. Birincisi, tekrar gelen olayda da başarılı yanıt dönün; hata dönerseniz pazaryeri yeniden dener ve sonunda webhook'unuzu pasifleştirir. İkincisi, ham gövdeyi saklayın. Olay işleyicinizde bir hata bulduğunuzda geçmişi yeniden oynatmanın tek yolu budur.
Rezervasyon Modeli
Stoğu doğrudan düşürmek yerine rezervasyon kaydı oluşturmak, iptal ve iade akışlarını çok basitleştirir. Fiziksel stok yalnızca depo hareketiyle değişir; sipariş yaşam döngüsü rezervasyon tablosunda yaşar.
| Rezervasyon durumu | Tetikleyen olay | Satılabilir stoğa etkisi |
|---|---|---|
acik | Sipariş düştü | Düşürür |
sevk_edildi | Kargoya verildi | Etkisiz (fiziksel stok da azaldı) |
iptal | Sipariş iptali | Geri verir |
tedarik_edilemedi | Pazaryerinde UnSupplied | Geri vermez, çünkü ürün gerçekten yok |
iade_bekliyor | İade talebi açıldı | Etkisiz |
iade_alindi | Ürün depoya girdi ve kontrol geçti | Fiziksel stoğu artırır |
Dördüncü satır sık atlanır. Tedarik edilemeyen bir siparişte rezervasyonu iptal sayıp stoğu geri verirseniz, olmayan ürünü yeniden satışa açarsınız ve aynı hatayı tekrarlarsınız. Beşinci ve altıncı satır ise şunu söylüyor: iade talebi açılması stok değildir. Ürün depoya girip kontrolden geçene kadar satılabilir stoğa eklemeyin; aksi hâlde müşteri iade etmekten vazgeçtiğinde elinizde olmayan bir ürünü satmış olursunuz.
Fiyat Senkronizasyonu Stoktan Farklı Kırılıyor
Yanlış stok bir satışı kaçırtır; yanlış fiyat doğrudan para kaybettirir. Üç pazaryerinin fiyat kuralları da birbirinden farklı ve hepsi doğrulama gerektiriyor:
- İki fiyat her zaman birlikte. N11'in dokümanı
listPricevesalePrice'ın birlikte gönderilmesini,listPrice'ınsalePrice'tan büyük olmasını şart koşuyor; değilse istek reddediliyor. Trendyol'un ürün oluşturma dokümanı da aynı kuralı yazıyor: "listPrice, salePrice'tan küçük olamaz". - Sıfır fiyat bir komuttur. Hepsiburada'da listelemeyi satışa kapatmak için stok ya da fiyat alanına sıfır göndermek gerekir. Fiyat hesaplama zincirinizde sıfır üretme ihtimali varsa (eksik maliyet kaydı, bozuk indirim kuralı, kur servisi düşmüş), o değeri servise ulaşmadan yakalayın.
- Ondalık biçimi. N11 ondalık ayıracının nokta olmasını ve virgülden sonra tam iki hane bulunmasını istiyor. Türkçe yerel ayarda çalışan bir formatlayıcı bu kuralı sessizce bozar.
- Barkod bazında fiyat güncelleme kotası. Trendyol bu işlem için SKU başına dakikada 30 istek sınırı koyuyor. Rakip takibi yapan bir fiyat motoru bu sınıra kolayca çarpar.
Fiyat gönderiminden önce çalışan bir "akıl sağlığı" kontrolü, bu dört maddeyi de tek yerde yakalar:
<?php
// Fiyat gonderim oncesi zorunlu kontroller. Tutarlar KURUS (int) olarak tasinir.
function fiyatiDogrula(int $listeKurus, int $satisKurus, int $sonSatisKurus): void
{
if ($listeKurus <= 0 || $satisKurus <= 0) {
throw new FiyatHatasi('Sifir veya negatif fiyat gonderilemez.');
}
if ($listeKurus < $satisKurus) {
throw new FiyatHatasi('listPrice, salePrice\'tan kucuk olamaz.');
}
// Ani sapma freni: son gonderilen fiyata gore %60'tan fazla dususe izin verme.
if ($sonSatisKurus > 0 && $satisKurus < (int) ($sonSatisKurus * 0.4)) {
throw new FiyatHatasi('Ani fiyat dususu: manuel onay gerekiyor.');
}
}
// Servise giderken: number_format($kurus / 100, 2, '.', '')
Ani sapma freni pahalı bir dersin ürünüdür: bir kur servisi düştüğünde ya da bir kampanya kuralı yanlış eşleştiğinde fiyat motoru dakikalar içinde tüm kataloğu yanlış fiyatla gönderebilir. Eşiği aşan değişiklikleri otomatik göndermek yerine onaya düşürmek, o riski ortadan kaldırır.
Sapma (Drift) Taraması Olmadan Sistem Kendini Düzeltmez
Ne kadar iyi kurarsanız kurun, kanaldaki değer ile sizdeki değer zamanla ayrışır: kaçan bir webhook, pasifleşen bir kuyruk işi, pazaryeri tarafında elle yapılan bir düzenleme. Bu yüzden günde en az bir kez tam mutabakat taraması gerekir: kanaldan mevcut stok ve fiyatı okuyun, kendi hesabınızla karşılaştırın, farkları raporlayın.
Taramanın farkı otomatik düzeltmesi cazip gelir ve tehlikelidir. Kanaldan okuduğunuz değer, o anda kuyrukta bekleyen bir güncellemeden önceki değer olabilir; körlemesine düzeltmek yeni bir yanlış yazma üretir. Güvenli davranış şudur:
- Fark tespit edildiğinde önce o SKU için bekleyen iş var mı diye bakın; varsa atlayın.
- Bekleyen iş yoksa yeni bir sürüm numarasıyla güncelleme kuyruğa alın; doğrudan API çağırmayın.
- Aynı SKU üst üste üç taramada sapıyorsa otomatik düzeltmeyi durdurup alarm üretin. Orada düzeltilmesi gereken bir hata vardır.
Trendyol tarafında bu tarama için bir ayrıntı önemlidir: toplu işlerin sonucu yalnızca 4 saat saklanıyor. Gece çalışan bir gönderim işinin sonucunu sabah raporunda sorgularsanız kayıt bulamazsınız. Sonuç sorgulamasını işin kendisine bağlayın.
Hangi Modeli Seçmeli?
| Model | Nasıl çalışır | Ne zaman seçilir |
|---|---|---|
| Tek havuz | Tüm kanallara aynı satılabilir stok gönderilir | Stok bolsa ve satış hızı düşükse. En basit, en çok satar, en çok aşırı satar. |
| Kanal kotası | Her kanala sabit pay ayrılır (örn. 40/30/30) | Dar stokta ve yüksek hızda. Aşırı satışı bitirir, karşılığında satış kaçırır. |
| Rezervasyonlu havuz | Ortak havuz + rezervasyon + hızdan türeyen emniyet payı | Varsayılan tercih. Kurulumu en pahalı olan, ama tek sürdürülebilir model. |
| Hibrit | Hızlı satan ürünlerde kota, diğerlerinde havuz | Katalogda birkaç ürün cironun çoğunu yapıyorsa. |
Pratik eşik şudur: bir ürünün stoğu, senkronizasyon penceresinde satılabilecek adedin 10 katından azsa o ürün kota modeline geçmelidir. Bunun üzerindeki ürünlerde havuz modeli yeterlidir ve daha fazla satış üretir.
Bu Şurada Kırılıyor
Kampanya motoru paralel yazıyor. Fiyatı hem fiyat motoru hem kampanya modülü hem de manuel panel değiştirebiliyorsa, hangisinin son sözü söylediği belirsizdir. Fiyatın tek bir sahibi olmalı; diğerleri o sahibin girdisini değiştirir, çıktısını değil.
Çok depolu stokta kanal tahsisi. İki deponuz varsa toplam stoğu göndermek yanlıştır; kanal o depodan sevk edemeyebilir. Kanal başına hangi depoların sayıldığını açıkça tanımlayın.
İade stoğu iki kez eklenir. Hem pazaryeri iade webhook'u hem depo giriş kaydı stoğu artırıyorsa ürün iki kere görünür. Fiziksel stoğu artırma yetkisini tek bir olaya verin: depo girişi.
Kuyruk birikmesi sessizdir. Kuyruk uzunluğunu izlemiyorsanız, gecikmenin büyüdüğünü ancak müşteri şikâyetinden öğrenirsiniz. Kanal başına "en eski bekleyen iş kaç saniyedir kuyrukta" metriğini panoya koyun; bu, emniyet payınızın yeterli olup olmadığını gösteren tek gerçek sinyaldir.
Saat dilimi. Trendyol sipariş sorgularında tarihler milisaniye cinsinden Unix zaman damgasıdır. Uygulamanız yerel saatle çalışıyorsa gece yarısı çevresinde üç saatlik bir boşluk oluşur ve o aralıktaki siparişleri kaçırırsınız. Zaman damgalarını her yerde UTC tutun, yalnızca ekranda çevirin.
Efor Bandı
ininia'da yazılım geliştirme 300-400 USD/adam-gün bandında, adam-gün dökümüyle fiyatlanır. Senkronizasyon katmanı için ayrı ayrı görmek isteyeceğiniz kalemler:
- Stok modeli: fiziksel / rezerve / satılabilir ayrımı ve rezervasyon durum makinesi
- Sürüm numarası, gölge durum tablosu ve delta gönderimi
- Kanal başına adaptör ve kota sayacı
- Webhook alıcısı, tekilleştirme ve yeniden oynatma aracı
- Fiyat doğrulama ve ani sapma freni
- Günlük sapma taraması ve fark raporu
- İzleme: kuyruk gecikmesi, başarısız kalem sayısı, sapma sayısı
Kendi kapsamınız için aralık görmek isterseniz proje fiyat hesaplama aracımızı kullanabilirsiniz.
Sıkça Sorulan Sorular
Stoğu gerçek zamanlı göndermek mümkün mü?
Hayır ve hedef de bu olmamalı. Pazaryeri tarafı zaten asenkron: Trendyol batchRequestId ile kuyruğa alıyor, N11 görevi IN_QUEUE durumunda bekletiyor. "Gerçek zamanlı" hedefi yerine ölçülen ve sınırlanan bir gecikme hedefleyin ve emniyet payını o gecikmeye göre hesaplayın.
Emniyet payını kaç yapmalıyım?
Formül: senkronizasyon penceresinin uzunluğu çarpı o ürünün o penceredeki en yüksek satış hızı. Bu iki sayıyı ölçebiliyorsanız payı hesaplarsınız; ölçemiyorsanız önce ölçün. Tüm katalog için tek bir sabit sayı, hem gereksiz stok kilitler hem de yetmez.
Aynı SKU'yu birden fazla pazaryerinde farklı fiyatla satabilir miyim?
Evet, her kanala ayrı listPrice/salePrice gönderirsiniz. Fiyat kuralını kanal adaptörüne değil, merkezi fiyat motorunuza yazın; adaptöre yalnızca hesaplanmış iki sayı gitsin. Kuralı adaptöre yazan sistemlerde aynı kural üç yerde ve zamanla birbirinden farklı biçimde yaşamaya başlar.
Webhook yerine periyodik tarama yeter mi?
Yalnız başına yetmez, ama webhook da yalnız başına yetmez. Trendyol'un webhook dokümanı, tekrarlayan başarısızlıkta webhook'un pasifleştirildiğini ve yedek olarak periyodik sipariş sorgulaması yapılmasını öneriyor. İkisini birlikte çalıştırın; tekilleştirme zaten çift kaydı engelleyecek.
Stok sıfıra düştüğünde ürünü silmeli miyim?
Hayır. Stoğu sıfır gönderin; ürün kaydı kalsın. Silip yeniden açmak ürünün pazaryerindeki geçmişini (yorum, satış verisi, sıralama) sıfırlar ve yeniden onay süreci gerektirebilir. Hepsiburada'da sıfır stok ya da sıfır fiyat zaten "satışa kapat" anlamına geliyor.
Kuyruk olarak Redis mi, veritabanı mı?
Kuyruk teknolojisi burada belirleyici değil; belirleyici olan kilit ve sürüm mantığıdır. Laravel'in atomik kilit özelliği Redis, Memcached, DynamoDB, veritabanı ve dosya sürücüleriyle çalışır. Kritik nokta, kilit sürücünüzün uygulama örnekleri arasında paylaşılıyor olmasıdır; her konteynerin kendi dosya önbelleğine yazdığı bir kurulumda kilit hiçbir şey korumaz.
Bir Sonraki Adım
Bugün yapılacak ölçüm şu: son 30 günde kaç siparişi tedarik edemediğinizi (pazaryerlerinde UnSupplied ya da muadili statü) sayın ve bunları SKU bazında gruplayın. Aşırı satış birkaç hızlı satan üründe yoğunlaşıyorsa çözüm tüm mimariyi değiştirmek değil, o ürünleri kota modeline almaktır. Tabana yayılmışsa senkronizasyon gecikmeniz büyük demektir ve önce kuyruk gecikmesini ölçmeniz gerekir.
Pazaryerlerinin API sözleşmelerindeki farklar için pazaryeri entegrasyonu yazımıza, sipariş sonrası sevkiyat tarafı için kargo entegrasyonu rehberimize bakabilirsiniz. Stok kaynağınız bir ERP ise ERP entegrasyonları sayfamız, genel kapsamımız için e-ticaret & perakende sayfamız işinizi görür. Kendi projeniz için iletişim sayfasından yazabilirsiniz.
Kaynaklar: Trendyol Geliştirici Dokümantasyonu (Stok ve Fiyat Güncelleme, Batch Request Sonucu, Ürün Oluşturma V2, Servis Limitleri, Webhook Model, Sipariş Paketi Listeleme sayfaları); Hepsiburada Geliştirici Portalı ("Listeleme Entegrasyonu Önemli Bilgiler", "Sipariş Entegrasyonu Önemli Bilgiler"); n11 Mağaza Destek Merkezi ("RestAPI Fiyat-Stok Güncelleme ve İşlem Sonucu Sorgulama Servisi"); Laravel resmî dokümantasyonu (Cache atomik kilitler, Queues / WithoutOverlapping ara katmanı). Tamamı 25 Eylül 2026'da okunmuştur.