# Pazaryeri Merkezi — API Referansı (v1)

Temel adres: `https://app.bayircommunity.com/api/v1`
Kimlik doğrulama: `X-Api-Key: bc_live_…` başlığı (veya `Authorization: Bearer bc_live_…`).
Gövde: JSON (`Content-Type: application/json`). Yanıt: JSON, her zaman `success` alanı içerir.
Hata biçimi: `{"success":false,"error":"validation|not_found|action_failed|marketplace_error|rate_limited|unauthorized","message":"…"}`.
Limit: üye başına dakikada 300 istek (`429` + `Retry-After`).

Bu API, üyelerin kendi e-ticaret yazılımı, WooCommerce ve Shopify eklentileri tarafından aynı şekilde kullanılır;
pazaryerine özgü hiçbir alan yoktur — tüm pazaryerleri tek biçime çevrilir.

Modüller: **Katalog** (ürün/stok/fiyat) · **Ürün açma** (pazaryerinde listeleme) · **Siparişler** · **Sipariş işlemleri** ·
**Kargo** (Yurtiçi Kargo gönderisi, takip) · **İrsaliye** (PDF/HTML) · **Fatura** (Logo İşbaşı e-Fatura/e-Arşiv, PDF yükleme, e-posta) · **İadeler** (stok kararı) · **Raflama** (depo yerleşimi: raflar, fiziksel stok, toplama, hareketler — hesap başına açılıp kapanır) · **Sorular** · **Finans** · **Bildirimler** · **Stok olayları** (çift yönlü) · **Profil** · **Sistem**.

---

## Sistem

| Uç nokta | Açıklama |
|---|---|
| `GET /status` | Hesap, bağlantı listesi, kuyruk sayaçları, sürüm |
| `GET /marketplaces` | Desteklenen pazaryerleri; her biri için `capabilities` (stock, price, orders, questions, answer, products, returns) ve `order_actions` (accept, pack, ship, invoice, cancel, deliver, cargo_change) |
| `GET /connections` | Üyenin bağlantıları (`id`, `marketplace`, `label`, `status`, `last_error`, ayarlar) |
| `POST /connections/{id}/test` | Pazaryeri kimlik bilgilerini doğrular |
| `POST /sync/now` | Kuyruğu beklemeden işletir (yanıt döndükten sonra arka planda) |
| `GET /batches` | Pazaryerine yapılan gönderim partileri ve sonuçları |
| `GET /dashboard` | Panel özeti: `counts{products, queued_updates, link_errors, orders_24h, orders_open, orders_unprinted, orders_unchecked, orders_shipped, orders_cost_missing, returns_open, returns_restock_pending, returns_restock_oldest_days, returns_restock_overdue, returns_restock_web, questions_waiting, questions_unread (v1.26.0: okunmamış gelen mesaj), unacked_events}` (v1.23.11: `returns_restock_*` = stok kararı bekleyen iade sayısı, en eskisinin kaç gündür beklediği, 2 günü geçen, kararı web sitesinde verilen web iadesi), `finance_month`, `last_30_days{revenue, orders, by_day[]}`, `connections[]` |
| `GET /logs?channel=&level=&connection_id=&since=&q=&page=&size=` | Hareket kayıtları (panel › Hareketler). Kanallar: orders, poll, push, cargo, invoice, webhook, api, connection, match … |
| `GET /order-items?applied=&connection_id=&since=&page=&size=` | Sipariş kalemleri ve stok düşüm izi (`stock_applied`: 0 yok, 1 düşüldü, 2 iade edildi) |
| `GET /api-keys` · `POST /api-keys` · `PUT /api-keys/{id}` · `DELETE /api-keys/{id}` | Ek API anahtarları: `POST {label, channel, rate_per_minute}` → `key` (bir kez gösterilir); `PUT {is_active, label, rate_per_minute}` |

## Pazaryeri bağlantıları (tam yönetim)

Panel › Pazaryerleri'nde yapılan her şey. Kimlik alanları pazaryerine göre değişir: `GET /marketplaces` → `credential_fields[] {name, label, required, type}`, `extra_settings{}`, `default_settings{}`,
`sku_index` (v1.23.3; kod eşleme dizini olan pazaryerlerinde `{label, unit, help, confirm}`, yoksa `null` — "yenile" düğmesi buna göre gösterilir).
`GET /connections` listesindeki her bağlantıda da `sku_index`: `{label, unit, help, confirm, built_at, count, complete, error, error_at, missing_cached}` (son tarama zamanı,
taranan kod sayısı, tarama tam mı, dizin kurulamadıysa hata metni, "pazaryerinde bulunamadı" önbelleğindeki kod sayısı) ya da `null`.

**Hepsiburada stok kodu (v1.23.3):** Hepsiburada stok kodlarını (merchantSku) BÜYÜK HARFE çevirerek kaydeder ("Küçük harf olarak gönderilen merchantSku
bilgisi büyük harfe dönüştürülerek … kaydedilmektedir"). Eşleşmedeki kod kataloğunuzdaki gibi kalır (ör. `ynck-diger-288-soft-beyaz`); stok ve fiyat
Hepsiburada'daki yazımla (`YNCK-DIGER-288-SOFT-BEYAZ`, Türkçe büyük harfle `…DİGER…` de olabilir) gönderilir. Kodun karşılığı listing listesinden
(`GET /listings/merchantid/{mid}`) kurulan dizinden bulunur; dizin `settings.code_index_ttl` saat (varsayılan 6) geçerlidir, yeni kod az ise tek tek
(`merchantSkuList`) aranır. Hepsiburada'da listing'i olmayan kod gönderilmez, eşleşmeye "Listing bulunamadı: … Stok kodlarını yenile" yazılır ve
`settings.code_miss_ttl` dakika (varsayılan 30) yeniden sorulmaz. Listing listesi okunamazsa gönderim durmaz: kod büyük harfe çevrilip gönderilir.

**Pazarama / PttAVM kodunda tire (v1.23.4):** bu pazaryerleri koddaki "-" işaretini kabul etmiyor: satıcı panelinde girilen
`ynck-diger-288-soft-beyaz` "-" işaretinden sonrası silinerek `ynck` olarak kaydedilebiliyor. Çözüm: ürünün pazaryerindeki kodunu tiresiz yazın
(`YNCKDIGER288SOFTBEYAZ`); kodlar **sade anahtarla** (harf / rakam dışı karakterler atılmış, harf duyarsız) eşleşir, katalogdaki kodunuz değişmez.
Sade anahtar yalnız tam eşleşme yoksa ve iki tarafta da tek karşılığı varken kullanılır (belirsizse "bulunamadı" denir, yanlış ürüne stok yazılmaz).
Pazarama'da dizin `GET /product/products` (onaylı + onaysız), PttAVM'de `StokKontrolListesi` (Barkod, UrunKodu, Gtin) ile kurulur; ikisinde de
`settings.code_index_ttl` (saat, varsayılan 6) ve `settings.code_miss_ttl` (dakika, varsayılan 60) vardır. Bulunamayan kodun hata metni ve
`sku-index` sonucu tiresiz kod önerisini verir; pazaryerinde kodu yalnız ilk parçadan oluşan ürün varsa (`ynck`) bu da yazılır. Kodu zaten kesilmiş
ürünler `match` ile **ürün adından** eşlenebilir (`by_name[]`). Siparişlerde ve iadelerde gelen tiresiz kodlar da sade anahtarla ürüne bağlanır.
PttAVM ek ayarları: `settings.cargo_profile_id` (ürün açarken kargo profili), `settings.vat_rate` (varsayılan KDV %, 20).

**PttAVM yeni API (v1.23.12):** PttAVM token tabanlı kimlik doğrulamayı zorunlu yaptı. Bağlantı kimlik alanları: `credentials.api_key` ve
`credentials.token` (ikisi zorunlu; PttAVM satıcı paneli › Hesap Yönetimi › Entegrasyon Bilgileri) — girilince bağlantı
`https://integration-api.pttavm.com` REST API'sini kullanır. `username` / `password` / `shop_id` isteğe bağlıdır (yalnız eski SOAP servisi;
API Key + Token girilmemiş eski bağlantılar). `PATCH /connections/{id}` ile `{"credentials": {"api_key": "…", "token": "…"}}` gönderip
`POST /connections/{id}/test` ile deneyin. Yeni API'de kod dizini `GET products/search` + toplu barkod araması (`products/get-by-barcodes`)
ile kurulur; stok / fiyat partisi `remote_id` = PttAVM takip numarası (trackingId). `GET /marketplaces` → `credential_fields` yeni alanları
(etiket, zorunluluk, `help`) verir.

| Uç nokta | Açıklama |
|---|---|
| `POST /connections` | `{"marketplace":"trendyol","label":"Mağazam","credentials":{"seller_id":"…","api_key":"…","api_secret":"…"},"environment":"prod","settings":{…}}` → kaydeder, ürünleri eşler (auto_link), bağlantıyı test eder. Yanıt: `connection` (detay), `test {ok, message}`. Zorunlu kimlik alanı eksikse 422 (`message` alan adını söyler). |
| `GET /connections/{id}` | Detay: `settings{sync_stock, sync_price, sync_orders, sync_questions, sync_returns, deduct_stock, auto_accept, auto_link, identifier (sku\|barcode), stock_buffer, max_stock, price_mode (same\|percent\|fixed), price_value, base_url, cargo_cost_default, orders_history_days, hook_lax, extra{}}` — **komisyon oranı ayarı yoktur**, komisyon her siparişte pazaryerinin bildirdiği tutardır, `credentials[] {name, label, required, set}` (değerler asla dönmez), `capabilities`, `order_actions`, `stats{links, queued, errors, orders}`, `inbound_webhook`, son hata / son başarılı zamanları |
| `PUT /connections/{id}` | `{"label":…, "environment":"prod|test", "is_active":true, "credentials":{…yalnız dolu gelen alanlar değişir…}, "settings":{…yalnız gönderilen anahtarlar…}}`. Gönderim kuralı (stok/fiyat/tampon) değişince eşleşmeler yeniden sıraya alınır (`requeued`), auto_link açılınca ürünler bağlanır (`linked`). |
| `DELETE /connections/{id}` | Bağlantı + eşleşmeler + o bağlantının sipariş/iade/soru kayıtları silinir (katalog ürünleri kalır) |
| `POST /connections/{id}/poll` | `{"what":"orders|returns|questions","days":90}` — şimdi çek (işçi meşgulse 409 `busy`). Kısayollar: `POST …/orders`, `…/returns`, `…/questions` |
| `POST /connections/{id}/history` | Sipariş geçmişini yeniden yükle (işçi her dakika bir parça) |
| `POST /connections/{id}/push` | Bekleyen stok/fiyat gönderimini şimdi yap (`deferred:true` = işçi meşgul, birazdan) |
| `POST /connections/{id}/resend` | Tüm aktif eşleşmeleri sıfırdan gönderime al (`queued`) |
| `POST /connections/{id}/match` | Pazaryerindeki ürünleri listeleyip katalogla (SKU / barkod) eşleştir: `remote, matched, unmatched, unmatched_sample[]`; `budget_seconds` (10–170). **v1.23.4:** tam eşleşmeyen kod **tiresiz / farklı ayraçlı** yazımla (sade anahtar: harf ve rakam dışı karakterler atılır, iki tarafta da TEK karşılık şartıyla) ve bu da tutmazsa **ürün adıyla** (iki tarafta da tek ürün) eşlenir. Ek alanlar: `by_compact` (sade anahtarla eşlenen sayısı), `by_name[]` (`{code, sku, title, truncated, suggest}` — adla eşlenenler, kontrol edilmeli), `truncated[]` (pazaryerinde kodu "-" işaretinden sonrası silinmiş görünen kodlar), `unmatched_sample[].truncated`. |
| `POST /connections/{id}/sku-index` | **Kod eşlemesini yenile** (Mirakl/Koçtaş: teklif SKU'ları, Pazarama: ürün kodları, **Hepsiburada (v1.23.3): listing stok kodları — merchantSku**, **PttAVM (v1.23.4): barkodlar**). Pazaryerindeki kodlar baştan taranıp harf duyarsız dizin kurulur, "bulunamadı" önbelleği silinir, eşleşmeler yeniden gönderime alınır: `offers, mapped, missing, missing_codes[], queued, message`; Pazarama / PttAVM'de (v1.23.4) ek olarak `suggestions` (`{bizdeki kod: pazaryerine yazılacak tiresiz kod}` — bulunamayan kodlar için öneri, ör. `ynck-diger-288-soft-beyaz` → `YNCKDIGER288SOFTBEYAZ`) ve `truncated` (`{bizdeki kod: pazaryerindeki kesik kod}` — pazaryerinde kodu yalnız ilk parçadan oluşan ürün varsa, ör. `ynck`). Tiresiz yazılmış kodlar kendiliğinden eşleşir (ör. "812 listing tarandı, 790 kod eşleşti, 22 kod pazaryerinde bulunamadı. Eşleşmeyenler: …"). Dizini olmayan pazaryerinde 422. Panel: bağlantı sayfası → düğme (Hepsiburada'da "Stok kodlarını yenile"). |
| `GET /connections/{id}/probe?what=orders|returns|questions` | Uç nokta teşhisi: `rows[] {name, ok, count, error, detail}` |
| `GET|PUT /connections/{id}/fees` · `GET|PUT …/price-rules` · `GET …/settlements` · `…/cargo-companies` · `…/cancel-reasons` · `…/return-reasons` | (aşağıdaki bölümler) |

---

## Katalog — ürün, stok, fiyat

### `POST /stock` (takma adlar: `/products/stock`, `/sync/stock`, `/products/sync`)
Tek istekte 1000 kaleme kadar. Veri anında kaydedilir (`202`), pazaryerlerine arka planda partiler hâlinde iletilir.
SKU yoksa ürün oluşturulur.

```json
{"mode":"auto", "items":[
  {"sku":"KOLTUK-01","barcode":"8680000000001","title":"3'lü Koltuk","stock":12,"price":4599.90,"list_price":5299.90}
]}
```
Yanıt: `{"success":true,"accepted":1,"created":0,"updated":1,"unchanged":0,"skipped":0,"queued_marketplace_updates":4,"errors":[]}`

- `mode`: `auto` (varsayılan) — sitenizin henüz onaylamadığı pazaryeri sipariş düşümleri gönderdiğiniz stoktan otomatik çıkarılır; `exact` — stok olduğu gibi yazılır.
- Kabul edilen alternatif alan adları: `quantity/qty/availableStock/stockQuantity`, `salePrice/salesPrice`, `listPrice/regular_price`, `name/product_name`.
- **`depo`** (gövdede, v1.22.1; varsayılan `true`): raflama açıkken yazılan satılabilir stok **depo (fiziksel) kaydını da taşır** — hedef fiziksel = satılabilir + stoktan düşülmüş toplanacak; artış `RAFSIZ`'a eklenir (rafsızda kalır), azalış önce rafsızın müsait kısmından, sonra müsait adedi en çok olan raftan düşülür (siparişe ayrılana dokunulmaz). Hareketler `adjust` (sayım), `sellable_after: null`, `ref: "stok-yazimi-depo"` ile yazılır ve `stock.moved` üretir. Yalnız yankı / toplu eşitleme gönderen istemci `"depo": false` ile kapatır (depo kaydı değişmez). `PUT /products/{sku}` gövdesinde de kabul edilir (ürün gönderimi). Hesap ayarı: `PUT /settings/features {"shelves_stock_sync": false}` ile tümden kapatılır.
- **`stock_delta`** (takma ad `delta`, imzalı tam sayı, ±1.000.000): mutlak `stock` yerine **fark** gönderin (`{"sku":"KOLTUK-01","stock_delta":-1,"ref":"WC-1001"}`). Yeni stok = bizdeki stok + fark (en az 0); sitenizin okuma-yazma arasında pazaryerinden düşen sipariş geri alınmaz. Her fark `GET /events`'e `type: adjust`, `source: site` olayı olarak düşer (`stock_after` = onaylanan yeni stok). `ref` verilirse aynı ürün + `ref` ikinci kez geldiğinde fark **uygulanmaz** (yeniden deneme güvenli), yanıtta `skipped` sayılır. `stock` ile birlikte gelirse mutlak `stock` geçerlidir.
- **`list_price: 0`** (ya da `"0"`) liste fiyatını **kaldırır** (`null`); alanı hiç göndermemek "değiştirme" demektir.
- SKU / barkod eşleşmesi büyük-küçük harf, aksan ve Türkçe harf duyarsızdır (`ABC-1` = `abc-1`, `İSTANBUL` = `istanbul`); kayıtlı yazım korunur.

### `POST /price` (takma ad: `/sync/price`) — sadece fiyat
```json
{"items":[{"sku":"KOLTUK-01","price":4499.00,"list_price":5299.90}]}
```

### `POST /products` veya `PUT /products` — ürün içeriği (toplu)
Stok/fiyata ek olarak ürün açmada kullanılan içerik alanları. Sadece gönderilen alanlar güncellenir.

```json
{"items":[{
  "sku":"KOLTUK-01","barcode":"8680000000001","title":"3'lü Koltuk Antrasit",
  "description":"<p>Ahşap iskelet…</p>","brand":"Örnek Mobilya","category":"Mobilya > Koltuk",
  "images":["https://…/1.jpg","https://…/2.jpg"],
  "attributes":{"Renk":"Antrasit","Kişi Sayısı":"3"},
  "vat_rate":20,"weight":45.5,"desi":120,
  "parent_sku":"KOLTUK","variant_attrs":{"Renk":"Antrasit"},
  "status":"active","stock":12,"price":4599.90,"list_price":5299.90
}]}
```
- `parent_sku` + `variant_attrs`: aynı `parent_sku` değerine sahip SKU'lar pazaryerinde tek ürünün varyantları olarak açılır.
- `images`: en fazla 12 URL. `attributes`: pazaryeri özellik adı → değer (kategori özelliklerine otomatik eşlenir).

### `PUT /products/{sku}` — tek ürün güncelle (aynı alanlar, `items` olmadan)

**Fiyat sahipliği:** fiyatın kaynağı üyenin kendi sitesidir. `POST /products` / `PUT /products/{sku}` ile gelen `price` / `list_price`
değeri katalogda değişirse, o ürünün **aktif pazaryeri eşleşmelerinin tamamı otomatik olarak gönderim sırasına alınır** (`dirty`);
böylece sitede yapılan fiyat değişikliği ilk gönderimde bütün pazaryerlerine yansır. Stoğun kaynağı ise SaaS'tır:
site stoğu `GET /products` ve `GET /events` ile okur.

### `GET /products?page=1&size=100&q=&updated_since=` — katalog listesi
### `GET /products/{sku}` — ürün detayı
Yanıtta `image` (pazaryerinden alınan ana görsel; panelde önbellekten servis edilir) ve `marketplaces[]` (bağlantı başına): `remote_code`, `pushed_stock`, `pushed_price`, `state` (`ok|sent|queued|rate_limited|error|pending|unknown`), `error`, `rate_limit`.

Liste ekranları için ek parametreler:

| Parametre | Açıklama |
|---|---|
| `skus=A,B,C` | Yalnız bu SKU'lar (en fazla 300). İstemcinin ekranındaki ürünlerin durumunu **tek istekte** almak için |
| `links=1` | Her ürüne `marketplaces[]` eklenir (hafif: `connection_id, marketplace, connection_label, remote_code, active, state, error, rate_limit, pushed_stock, pushed_price, pushed_at, price_override, list_price_override, check`). **v1.21.0**: pazaryerine özel fiyatlar (`price_override`, `list_price_override`) ve istek sınırı bilgisi (`rate_limit`) bu hafif çıktıya da eklendi; `state` artık istek sınırında `rate_limited` döner ve o sırada `error` boştur (hata değil, sırada). |
| `state=error\|queued\|rate_limited\|ok\|mismatch\|unlinked` | Pazaryeri eşleşme durumuna göre süz (`unlinked` = hiç aktif eşleşmesi yok, `mismatch` = kontrolde stok/fiyat uyuşmuyor, `rate_limited` = pazaryeri istek sınırı yüzünden sırada — **hata değil**) |
| `connection_id=3` | Yalnız o bağlantıya eşleşmiş ürünler |
| `details=1` | **v1.23.1**: her ürüne kartın özet alanları eklenir: `cost` (KDV dahil maliyet), `vat_rate`, `desi`, `weight`, `parcel_count`, `parent_sku`, `status` (`active\|passive`), `content_updated_at`. Açıklama / görsel / özellik gelmez (tek ürün ucunda). Bağlı site kendi kataloğunu bununla karşılaştırır, yalnız farklı olanları gönderir. |
| `status=active\|passive` | **v1.23.1**: ürün durumuna göre süz |
| `cost=missing\|set` | **v1.23.1**: maliyeti boş / girilmiş ürünler |

### `GET /products/issues?limit=20` — senkron sorunları özeti
`{"counts":{"error":3,"queued":12,"rate_limited":14,"unlinked":5,"mismatch":2,"products":410},"items":[{"sku","title","connection_id","marketplace","connection_label","remote_code","state","error","rate_limit","pushed_stock","pushed_price","pushed_at","check"}]}` — hatalı eşleşmeler önce.

**Pazaryeri istek sınırı (rate limit) hata değildir.** Pazaryeri "şu kadar süre sonra tekrar deneyin" dediğinde kalem `state="rate_limited"` olur, `error` boş döner ve `rate_limit{until, seconds, minutes, note}` alanı ne zaman yeniden denendiğini söyler. Kalemler kuyrukta kalır, süre dolunca **en güncel** stok/fiyatla tek pakette kendiliğinden gönderilir; kullanıcının bir şey yapması gerekmez. Bekleme payı ve üst sınırı yönetici panelindeki Ayarlar ekranından değiştirilir.

### `POST /products/{sku}/resend` — ürünü yeniden gönderime al
Gövde isteğe bağlı: `{"connection_id":3}` (boşsa tüm aktif eşleşmeler). Değerler değişmemiş olsa bile gönderim kuyruğuna alır ve hata kaydını temizler. Yanıt: `{"queued":n,"product":{…}}`.

Görseller: ürün görselleri pazaryerlerinden otomatik toplanır (sipariş kalemlerindeki görsel alanları + günde bir pazaryeri ürün listesi),
sunucuda küçültülerek önbelleğe alınır (`storage/cache/img`) ve panelde `index.php?p=img&u=…` adresinden anında servis edilir.

### `POST /products/{sku}/rename` — ürün kodunu (SKU) değiştir (v1.23.1)
`{"sku":"YENI-KOD"}` — ürün satırı aynı kalır: stok, raf kayıtları, pazaryeri eşleşmeleri ve sipariş geçmişi bu üründe kalır, yalnız kod değişir.
Yeni kod başka bir üründe varsa `409 conflict` (`conflict_sku`) döner — iki ürün birleştirilmez. Yalnız harf büyüklüğü / Türkçe harf farkı
varsa (aynı kod sayılır) yazım güncellenir. Yanıt: `{"renamed":true,"product":{…}}`. Bağlı site, ürünün SKU'su sitede değiştirilince bunu çağırır.

**Site yetkili katalog (e-ticaret sitesi 1.11.1):** site ürün bilgilerini (ad, barkod, maliyet, KDV, desi, ağırlık, durum, fiyat, varyant)
`POST /products` ile `{"create":false,"depo":false,"items":[…]}` gövdesiyle gönderir (stok alanı yok — stok ayrı yoldan gider);
sitede olmayan ürünleri `{"sku":…,"status":"passive"}` ile pasife alır (silmez: stok, raf ve pazaryeri eşleşmeleri korunur; `status`
yalnız bilgi amaçlıdır, pazaryeri gönderimini durdurmaz).

### `POST /products/delete` — toplu silme (v1.23.2)
```json
{"skus":["ESKI-01","ESKI-02"],"ids":[15,16],"skip_linked":true,"dry_run":true}
```
Ürünleri katalogdan kalıcı olarak siler (tek istekte en çok 2000 kod; `skus` ve/veya `ids`). **Pazaryerlerindeki ilanlar silinmez.**
- Silinen ürünün pazaryeri eşleşmesi, **aynı barkodlu** (ya da ilan koduyla aynı kodlu) başka bir ürüne aktarılabiliyorsa aktarılır:
  eşleşme satırı olduğu gibi (pazaryerine özel fiyatıyla) o ürüne geçer ve gönderim sırasına alınır — stok o üründen güncellenmeye
  devam eder (kodu değişen ürün grubunun eskisi silinirken ilan yenisine geçer). O üründe aynı ilan zaten varsa eşleşme silinir (`already`).
- Aktarılamayan ilan **sahipsiz** kalır (stoğu artık güncellenmez). `skip_linked:true` iken böyle ürünler silinmez (`action:"skip"`).
- Açık (yeni / hazırlanıyor) siparişte kalemi olan ürün silinmez (`action:"skip"`, `reason` sipariş numarasıyla) — kargoya verilince silinebilir.
- Raf kayıtları silinir; stok hareket geçmişi (SKU ile) ve sipariş geçmişi kalır. Pasif eşleşmeler ürünle birlikte silinir.
- `dry_run:true`: hiçbir şey değişmez, aynı rapor döner (onay penceresi için).

Yanıt: `{"deleted":2,"relinked":1,"unmanaged":1,"skipped":0,"shelf_qty":7,"not_found":["YOK-1"],"dry_run":false,
"items":[{"sku":"ESKI-01","title":"…","action":"delete|skip","reason":null,"shelf_qty":0,"links":[{"marketplace":"Hepsiburada","remote_code":"HB-1","to":"YENI-01","result":"moved|already|unmanaged"}]}]}`.
Boş liste ya da 2000'den fazla kod: `422`. Panelde aynısı: Ürünler › seçim kutuları › "Seçilenleri sil" (aramaya uyan tümü de seçilebilir; önce önizleme).
Web sitesi (e-ticaret sitesi 1.11.2) "Pazaryeri Merkezi'nden de sil" seçilerek silinen ürünleri bu uçla siler.

### `POST /products/verify` — pazaryeri kontrolü
`{"connection_ids":[3,4]}` (boşsa tüm bağlantılar). Pazaryerindeki ürün listesi çekilir, eşleşmiş ürünlerin stok/fiyatı bizimkiyle karşılaştırılır; sonuç `GET /products/{sku}` çıktısında
`marketplaces[].check` (`status`: ok|stock|price|both|missing, `note`, `checked_at`).

### `PUT /products/{sku}/prices` — pazaryerine özel fiyat
```json
{"trendyol":{"price":4799.00,"list_price":5299.00},"n11":4699,"3":null}
```
Anahtar: pazaryeri kodu veya bağlantı id'si; `null` özel fiyatı kaldırır (genel fiyat + bağlantı fiyat kuralı geçerli olur). `POST /stock` / `POST /products`
kalemlerinde `"marketplace_prices":{"trendyol":4799}` ile de gönderilebilir. Ürün çıktısında `marketplaces[].price_override` görünür.
**v1.21.0**: değerler burada da doğrulanır — negatif / sıfır / sayı olmayan girişler `errors[]` içinde Türkçe açıklamayla geri döner ve o bağlantı **atlanır** (eskiden sessizce 0 ₺ yazılıyordu).
Tek bir pazaryerini kodu ile birlikte düzenlemek için `PUT /products/{sku}/links` daha uygundur.

### Excel ile toplu ürün
Panel › Ürünler › "Şablonu indir" (.xlsx) → doldur → yükle. Sütunlar: SKU*, Barkod, Ürün adı, Stok, Satış/Liste fiyatı, Marka, Kategori, KDV, Maliyet, Desi, Koli adedi, Ağırlık, Açıklama, Görsel 1–5, Ana ürün kodu, Varyant: Renk/Beden, Özellikler (Ad=Değer; …), Durum. API: `GET /products/template.xlsx` (şablon), `POST /products/import` (multipart `file` ya da `{"file_base64":"…","file_name":"urunler.xlsx"}`; xlsx/csv) → `rows, parse_errors, result{created, updated, errors}`. JSON karşılığı `POST /products`.

### Diğer ürün işlemleri
| Uç nokta | Açıklama |
|---|---|
| `DELETE /products/{sku}` | Ürünü katalogdan siler (eşleşmeleriyle); pazaryerlerindeki ürünler etkilenmez. Toplu silme ve ilan aktarımı için `POST /products/delete` |
| `POST /products/{sku}/image-pull` | Bağlı pazaryerlerinden görsel arar, ilk bulunanı ürün kartına yazar: `image, source, cached, results[]` (404: hiçbirinde yok) |
| `POST /products/images/pull` | Görseli olmayan tüm ürünler için pazaryerlerinden görsel çeker (`budget_seconds` 10–90; kalanı arka plan işçisi tamamlar) |
| `GET /products/labels` · `GET /products/labels.pdf` | **Ürün barkod etiketi (A4 sayfa).** `skus=SKU1,SKU2` (ya da barkod) · `count=12` (her üründen kaç etiket, varsayılan 1) · `per_page=12\|24\|40` (A4'e 2/3/4 sütun) · `title=1\|0` (ürün adı) · `price=1\|0`. Barkod içeriği ürünün barkodu, yoksa SKU (Code-128). HTML yanıtta yazdırma araç çubuğu vardır. |
| `GET /products/{sku}/labels` · `.pdf` | Tek ürün için aynısı (`count`, `per_page`, `title`, `price`) |

### `PUT /products/{sku}/links` — manuel eşleştirme + pazaryerine özel fiyat
```json
{"connection_id":3,"remote_code":"8680000000001","active":true,"price_override":4799.90,"list_price_override":5299}
```
`remote_code`: pazaryerindeki tanımlayıcı (Trendyol/HB/Koçtaş: barkod; N11/Pazarama/Çiçeksepeti/İdefix/PttAVM: stok kodu ya da pazaryeri ürün id'si).
Kaydedince eşleşme gönderim kuyruğuna alınır ve **önceki hata kaydı temizlenir** (`state` → `pending`, `last_error` → boş). `active:false` verilirse o pazaryerine stok / fiyat gönderilmez.

**v1.21.0 — `price_override` / `list_price_override`** (bu pazaryerine özel satış / liste fiyatı):

| Gövdede | Sonuç |
|---|---|
| alan **hiç yok** | mevcut özel fiyat **korunur** (yalnız kod / aktiflik güncellenir) |
| `null` ya da `""` | özel fiyat **kaldırılır**: genel fiyat + bağlantının fiyat kuralı uygulanır |
| sayı (`4799.90`, `"4.799,90"`, `"4 799,90 TL"`) | o pazaryerine bu fiyat gönderilir, **fiyat kuralı atlanır** |

Doğrulama (hepsi `422` + `error:"validation"` + Türkçe `message`): negatif değer ("… negatif olamaz"), sıfır ("… sıfırdan büyük olmalı"),
sayı olmayan değer ("… sayı olmalı: "abc" okunamadı"), sütuna sığmayan değer ("… çok büyük"). Reddedilen istek **hiçbir alanı değiştirmez**.
Değer kabul edilince kalem gönderim sırasına alınır (`dirty`), böylece yeni fiyat ilk gönderimde pazaryerine gider.
Aynı düzenleme toplu biçimde `PUT /products/{sku}/prices` ile de yapılabilir.

### v1.23.8 — Sipariş: "teslim edilemedi" ve iade süreci iade değil
- "Teslim edilemedi" ara durumu (Trendyol `UnDelivered`, Pazarama 14, İdefix `shipment_undeliver`, Çiçeksepeti / PttAVM "Teslim Edilemedi":
  kargo müşteriye ulaşamadı, gönderi şubede / tekrar dağıtımda) artık `status: "shipped"`, `status_label: "Kargoda · teslim edilemedi"`,
  yeni alan `undelivered: true`. Paket satıcıya geri dönünce (Trendyol `Returned`, Çiçeksepeti "Firmaya İade Edildi") `status: "returned"`.
- Hepsiburada `Undelivered` kesin iadedir (sipariş iptal / para iadesi): `status: "returned"`, `status_label: "İade · teslim edilemedi"`.
- Müşteri iade süreci / reddi / talep ("İade Talebi", "İade Süreci Başlatıldı", "İade Reddedildi", Hepsiburada `ClaimCreated`, Koçtaş
  `INCIDENT_OPEN` …): `status: "delivered"`, `status_label` "Teslim edildi · iade sürecinde" / "· iade reddedildi" / "· iade edildi" /
  "· talep açık". Sipariş düzeyinde stok oynamaz; iade ve stok kararı iade kaydında (`/returns`).
- Önceki sürümlerde `returned` / `cancelled` görünen bu siparişler güncellemede bir kez düzeltilir (sipariş geçmişinde `action: "status_fix"`).

### v1.23.7 — Eşleşme durumu: hangi ürün hangi pazaryerinde eşli / eşleşmemiş / hatalı

Bağlantı kartındaki "eşli ürün" yalnız bir sayıydı ("Kimisinde 140, kimisinde 139 — aradaki fark hangisi?"). `GET /links/matrix`
her ürünü × her bağlantıyı tek tabloda verir. Panel: Pazaryerleri / Ürünler › **🔀 Eşleşme durumu** (`?p=coverage`); site
(e-ticaret sitesi 1.11.7): Pazaryerleri › Eşleşme Durumu. Mantık tek yerde: `App\LinkMatrix`.

**`GET /links/matrix`** — parametreler:

| Parametre | Açıklama |
|---|---|
| `filter` | `all` (varsayılan) · `differs` — bazı bağlantılarda eşli, bazılarında değil (**aradaki fark**) · `missing` — aktif eşleşmesi olmayan · `error` — pazaryeri hata verdi · `waiting` — sırada ya da sonuç gelmedi · `check` — pazaryeri kontrolü uyuşmazlığı |
| `connection_id` | **Odak** bağlantı: `missing` / `error` / `waiting` / `check` yalnız ona bakar (`differs` her zaman kapsamdaki bütün bağlantılara) |
| `connections` | Kapsam (tablonun sütunları): `3,5` — boşsa bütün bağlantılar (durdurulmuşlar dahil) |
| `q` | SKU, barkod, ürün adı ya da **pazaryeri kodu** içinde arama (harf duyarsız; `%` `_` joker değildir) |
| `status` | `active` · `passive` (ürün durumu; pasif = sitede yok) |
| `page`, `size` | Sayfa; `size` en çok 500 (varsayılan 100). Son sayfadan büyük sayfa son sayfaya çekilir |

Yanıt: `connections[] {id, marketplace, marketplace_label, label, name, active, environment, auto_link, identifier, rate_limit, stats}`,
`products_total`, `filter`, `connection_id`, `q`, `status`, `counts {all, differs, missing, error, waiting, check}` (arama / ürün durumu / odak
dikkate alınır), `page`, `size`, `total`, `total_pages`, `labels` (durumların Türkçe adları), `items[] {id, sku, title, barcode, status, stock, price,
image, linked (kaç bağlantıda aktif eşli), connections (kapsamdaki bağlantı sayısı), cells {"<bağlantı id>": hücre}}`.

`stats` (katalogun tamamı üzerinden): `products`, `linked` (= kartın "eşli ürün"ü), `missing` (= products − linked), `none` (eşleşmesi hiç
açılmamış), `inactive` (pasif eşleşme), `error`, `queued`, `unknown`, `check`. Aynı sayılar `GET /connections/{id}` → `stats` içinde de
(`links`, `missing`, `inactive`, `none`, `products`, `unknown`, `check`).

Hücre: `state` = `none` (eşleşme yok) · `inactive` (pasif — bu pazaryerine gönderilmiyor) · `queued` · `rate_limited` · `pending` · `sent` ·
`ok` · `error` · `unknown`; `label`, `code`, `active`, `error` (pazaryerinin mesajı; hata sonrası yeniden sıraya girmişse "Önceki gönderim
hatası: …"), `check {status, label, note, at}`, `queue` (v1.23.6 ile aynı), `rate_limit`, `pushed_at`, `pushed_stock`, `pushed_price`,
`price_override`, `list_price_override`; `none` hücresinde **`suggest`** (bağlantının kimlik ayarına göre önerilen kod: barkod ya da SKU).

`error` sayımı: aktif eşleşme + `state = error`. Kullanıcı "yeniden gönder" dediyse (sırada, hata metni silinmiş) sonuç gelene kadar hatalı
sayılmaz; stok / fiyat değiştiği için yeniden sıraya giren (hata metni duruyor) hatalı sayılır.

**`POST /links/cell`** — tek hücre: `{"sku":"…","connection_id":3,"op":"link|code|activate|deactivate|resend","remote_code":"…"}`

| `op` | Ne yapar |
|---|---|
| `link` | Eşleşme aç (kod zorunlu), aktif |
| `code` | Kodu değiştir (gönderim sırasına alınır) |
| `activate` | Pasif eşleşmeyi aktif et (kod verilirse o kodla); eski kontrol notu silinir |
| `deactivate` | Pasife al: bu pazaryerine artık gönderilmez (pazaryerinden silinmez) |
| `resend` | Aktif eşleşmeyi yeniden gönder (hata metni silinir) |

Kurallar `POST /links/bulk` ile aynı (aynı bağlantıda aynı kod iki aktif eşleşmede olamaz → `422` + neden). Yanıt: `message`, `item`
(ürünün güncel satırı, `GET /links/matrix` biçiminde), `summary`.

**Excel** — `GET /links/export.xlsx?filter=missing&connection_id=3` (ürün durumu için `status_filter=active|passive`, arama `q`):
süzgeçteki ürünler; odak verilirse yalnız o bağlantının sütunları. `filter` verilmezse eski davranış.

**"Ürünleri eşleştir"** (`POST /connections/{id}/match`): pazaryerinin listesinde bulunamadığı için pasife alınan eşleşmelere neden yazılır
(`check.status = "missing"`, `check.note = "Ürünleri eşleştir (tarih): ürün pazaryerinin ürün listesinde bulunamadı, eşleşme pasife alındı."`);
yanıtta `deactivated` (daha önce aktifken pasife alınan ürün sayısı) ve mesajda bu sayı.

### v1.23.6 — Sıradaki eşleşmenin nedeni (`queue`) · sonucu bildirilmeyen kalemin doğrulanması

**`queue` alanı** — `GET /products/{sku}`, `GET /products?links=1` ve `GET /products/issues` yanıtlarındaki her `marketplaces[]` / `items[]` kaleminde.
Eşleşme gönderim sırasında değilse `null`; sıradaysa:

| Alan | Anlamı |
|---|---|
| `since`, `minutes` | Kaç dakikadır sırada (`dirty_since`) |
| `next_try` | Bağlantının bir sonraki gönderim denemesi (gelecekteyse) |
| `failures` | Bağlantının art arda başarısız gönderim sayısı |
| `error`, `error_at` | Gönderim art arda başarısızken bağlantının son hatası; bağlantı durdurulmuşsa "Bu pazaryeri bağlantısı durdurulmuş…" |
| `paused` | Bağlantı durdurulmuş (`is_active = 0`) |

`GET /products/issues`: 30 dakikadan uzun sırada bekleyen ve bağlantısı art arda hata alan (ya da durdurulmuş) eşleşmeler de listelenir;
`error` = `"Sırada bekliyor — <bağlantının son hatası>"`.

**Sonuç gelmedi (`state = "unknown"`)** — pazaryeri partinin sonucunda kalemi anmadıysa. Pazarama'da (parti kimliği tanınmadı, BSK16) artık
Pazarama'daki ürün **okunur**: stok ve satış fiyatı gönderilenle aynıysa kalem `ok` olur (`check.status = "ok"`, `check.note` =
"Pazarama sonucu bildirmedi; üründe stok … okundu, gönderilenle aynı"). Uyuşmuyorsa parti ilk 5 dakika açık tutulup yeniden bakılır; sonra
`unknown` olur ve `error` Pazarama'da ne göründüğünü yazar ("Pazarama'da şu an stok 34 / 29.999,00 ₺ görünüyor (gönderilen: …)"),
`check.status` = `stock` | `price` | `both` | `missing`.

**Kod önerisi kaldırıldı** — `POST /connections/{id}/sku-index` yanıtında `suggestions` artık boş (`truncated` sürer); `POST /connections/{id}/match`
yanıtında `by_name[].suggest` boş. `GET /links/export.xlsx`: "Tiresiz kod" bilgi sütunu yalnız `safe=1` ile gelir.

### v1.23.5 — Pazaryeri kodları TOPLU (Excel ve JSON)

Kodları ürün ürün girmek yerine: **indir → Excel'de düzenle → yükle (önce önizleme) → uygula**. Panel: Ürünler › **📑 Pazaryeri kodları (Excel)**
(`?p=links`); site: Pazaryerleri › Kodlar (Excel). Hepsi aynı mantıktan (`App\LinkSheet`) geçer.

| Uç nokta | Açıklama |
|---|---|
| `GET /links` | Tüm eşleşmeler: `items[] {sku, title, barcode, connection_id, marketplace, connection_label, remote_code, active, price_override, list_price_override, state, error, status_text, pushed_at}`, `total`. Süzgeç: `connection_id`, `sku`; `page`, `size` (en çok 5000, varsayılan 1000) |
| `GET /links/export.xlsx` | Excel: her ürün bir satır, her bağlantı bir sütun (`"Pazarama kodu [#3]"`). Seçenekler: `connections=3,5` (boş = hepsi) · `products=all\|linked\|unlinked\|error` · `prices=1` (özel fiyat + özel liste fiyatı sütunları) · `active=1` (Evet/Hayır) · `status=1` (gönderim durumu, bilgi) · `safe=1\|0` (tiresiz kod bilgi sütunu; verilmezse Pazarama / PttAVM varsa açık). İkinci sayfa "Kılavuz". |
| `POST /links/import` | Düzenlenen dosya: multipart `file` ya da `{"file_base64":"…","file_name":"kodlar.xlsx"}` (CSV de olur). **`"dry_run": true` → yalnız önizleme, hiçbir şey yazılmaz.** Yanıt: `message, summary {rows, new, code, price, active, same, errors, changes}, applied, changes[] {row, sku, connection_id, connection, actions[], old, new}, changes_total, errors[] {row, sku, conn, connection, message}, errors_total, warnings[], columns[], ignored_columns[]` (listelerde ilk 1000) |
| `POST /links/bulk` | JSON: `{"items":[{"sku":"…","connection_id":3,"remote_code":"…","price_override":4799.9,"list_price_override":null,"active":true}],"dry_run":false}` — alan gönderilmezse değişmez; `price_override: null` özel fiyatı kaldırır. Yanıt `/links/import` ile aynı. En çok 20000 kalem |

Kurallar: dosyada **boş hücre değiştirmez**; özel fiyat hücresine `-` (ya da `sil`) → özel fiyat kaldırılır; aktif `Evet/Hayır`. SKU harf duyarsız
bulunur. Bir bağlantıda **aynı kod iki aktif eşleşmede olamaz** (harf duyarsız; dosyanın SON durumuna bakılır, A↔B takası serbest) — çakışan satır
uygulanmaz ve sebebi `errors[]`'da yazar. Kodu değişen eşleşme `PUT /products/{sku}/links` gibi gönderim sırasına alınır (eski hata / kontrol notu
silinir). Eşleşmesi olmayan üründe özel fiyat / aktif için önce kod gerekir. Başlıktaki `[#3]` bağlantı numarasıdır; silinirse bağlantı adı /
pazaryeri adı tek bağlantıya uyuyorsa sütun yine tanınır. Kod hücresi Excel'de sayıya dönmüşse (`8.69E+12`) satır reddedilir. Uygulamalar
Kayıtlar'a (`catalog`) yazılır; uygulanmayan satırlar uyarı olarak da düşer.

`POST /products/import` (ürün Excel'i) ve panelin ürün Excel yüklemesi, dosyada aynı düzende pazaryeri sütunları varsa ürünleri ekledikten sonra
kodları da eşler (yanıtta `links`).

---

## Ürün içeriği hakkında

Bu sürümde pazaryerine ürün açma / içerik güncelleme (kategori, özellik, açıklama gönderimi) **kaldırılmıştır**; sistem stok, fiyat, sipariş,
kargo, fatura ve iade yönetimine odaklanır. Ürün kartındaki içerik alanları (`POST /products` ile gönderilen başlık, görsel, açıklama…)
yalnızca kendi kayıtlarınız / irsaliye için saklanır.

---

## Siparişler

### `GET /orders`
Ek filtreler: `cost_missing=1` (maliyeti girilmemiş kalemi olan siparişler), `pack=unprinted|unchecked|checked` (irsaliye basılmamış / paket kontrolü yapılmamış / yapılmış), `local_status=carrier_set|delivery_reported|packed|shipped|…`, `parent_order_id=` (bir siparişin ek gönderileri). Sipariş detayında `children[]` (bağlı manuel gönderiler), `parent_order_id`, `user_fields[]` (elle kilitlenmiş alıcı alanları), `picked_at` / `picked_by` (raflama: raftan toplandı) ve raflama açıksa `pick{state, items[]}` + her kalemde `locations[] {location, quantity, status}` (bkz. Raflama).
Parametreler: `status` (virgülle: `new,processing,shipped,delivered,cancelled,returned`), `marketplace`, `connection_id`,
`updated_since`, `since`, `until` (sipariş tarihi), `order_number`, `q` (sipariş no/müşteri/takip no/kargo barkodu/paket no), `sort=order_date|updated_at`,
`page`, `size` (≤200), `include=items`.

Değişiklikleri düzenli almak için: `GET /orders?updated_since=<son kontrol>&sort=updated_at&include=items`.

Sipariş nesnesi:
```json
{"id":15,"marketplace":"trendyol","connection_id":3,"order_number":"123456789","package_number":"98765",
 "status":"processing","status_label":"Hazırlanıyor","marketplace_status":"Picking","local_status":null,
 "order_date":"2026-09-10 14:22:00","customer":"A*** Y***","email":"…","phone":"…",
 "shipping_address":{"name":"…","address":"…","city":"İstanbul","district":"Kadıköy","postal_code":"…"},
 "billing_address":{…},"tax_number":null,"company":null,
 "cargo_company":"Yurtiçi","tracking_number":"…","tracking_url":"…","cargo_barcode":"…",
 "total":4599.90,"currency":"TRY","invoice_number":"","invoice_url":"","invoice_sent_at":null,"shipped_at":null,"local_status":"",
 "parcel_count":2,"desi":12.5,"note":"Kırılacak eşya","cargo_cost":null,"commission":null,"shipment_id":18,
 "shipment":{"id":18,"carrier":"yurtici","cargo_key":"YRK123456789","tracking_number":"1002131026","status":"in_transit",…},
 "items":[{"id":40,"line_id":"555","sku":"KOLTUK-01","catalog_sku":"KOLTUK-01","barcode":"…","name":"…","quantity":1,"unit_price":4599.90,"total":4599.90,"status":"Picking","stock_deducted":true}],
 "first_seen_at":"…","updated_at":"…"}
```

**Takip no ve kargo barkodu (v1.23.3):** `tracking_number` kargo firmasının TAKİP numarasıdır, `cargo_barcode` paketin KARGO BARKODUdur (etikete basılan
kod; Hepsiburada'da "teslimat numarası"). Hepsiburada'da paket listeleri yalnız barkodu verir; takip numarası ve `tracking_url` paket sorgusundan
(`GET /packages/merchantid/{mid}/packagenumber/{no}` → `trackingInfoCode`, `trackingInfoUrl`) kargo firması paketi teslim alınca gelir: işçi kargodaki
siparişleri numara gelene kadar 30 dakikada bir sorar (kargoya verilişten 30 gün; teslim edilmiş ama numarası alınmamış sipariş bir kez). Numara gelince
işlem geçmişine `tracking` ("Kargo takip numarası: …") yazılır. Paket bozulursa (Hepsiburada "Unpacked") sipariş "Yeni"ye döner; `package_number`,
`cargo_barcode`, `tracking_number` temizlenir. İrsaliyedeki barkod `cargo_barcode` (yoksa takip no, o da yoksa sipariş no); paket kontrolü
(`POST /orders/pack-check`) kargo barkoduyla da bulur.

**Son kargo tarihi ve indirim (v1.26.0):**
- `ship_by` (`"Y-m-d H:i:s"`, Türkiye saati | `null`): siparişin en geç kargoya verilmesi gereken an.
- `ship_by_source`: `mp` (pazaryeri bildirdi) ya da `calc` (tahmin). Pazaryerlerinin bildirdiği alanlar:
  - Koçtaş: `shipping_deadline`
  - İdefix: kalemlerin en erken `lastShipmentDate`'i
  - Trendyol: `extendedAgreedDeliveryDate`, yoksa `agreedDeliveryDate`
  - Hepsiburada: kalem `dueDate`'i
  - Tarih bildirmeyen pazaryerlerinde tahmin yapılır: bağlantı ayarındaki **hazırlık süresi** (iş günü) ve **kesim saati**. Bağlantı ayarı `ship_days` / `ship_cutoff_hour`; 0 = tahmin yok.
- `ship_by_state`: şu değerlerden biri; kargoya verilmiş, teslim, iptal ya da iade siparişte `null`.
  - `overdue`: süresi geçti
  - `today`: bugün son gün
  - `soon`: uyarı süresi içinde. Varsayılan 24 saat; hesap profili `orders.ship_warn_hours`, `PUT /profile` → `{"orders":{"ship_warn_hours":12}}`.
  - `ok`
- `GET /orders?sort=ship_by`: en acil önce, tarihsizler sonda.
- `GET /orders?due=overdue|today|soon|open`: `open` = geçen + bugün + yaklaşan.
- `GET /dashboard` sayaçları: `orders_overdue`, `orders_due_today`.
- `discount_total`: siparişin indirim toplamı (pazaryerinin bildirdiği). `total` artık **müşterinin ödediği** tutardır.
- Kalemlerde `list_price` (indirimsiz birim fiyat) ve `discount` (kalem indirimi) var; `price` / `total` ödenen tutardır.
- İdefix'te önceden indirimsiz fiyat yazılıyordu. 1.26.0 güncellemesi eski İdefix siparişlerini ham veriden yeniden hesaplar. Kesilmiş faturalara dokunulmaz; tutarı farklı olanlar panel › Faturalar'da listelenir.
- Trendyol `orderDate` "GMT +3 damgası" olarak geldiği için sipariş saati 3 saat ileri görünüyordu. Düzeltildi; eski siparişler de güncellemeyle düzeltilir.

**İdefix kargo profili (v1.26.0):** `GET /connections/{id}/cargo-companies` İdefix satıcı kargo profillerini döner. `code` = profil id; etikette "İdefix anlaşmalı / kendi anlaşmam / alternatif" yazar.
- `POST /orders/{id}/cargo {"cargo_company":"<profil id>"}`: kargo firmasını İdefix'te değiştirir. Yeni kargo kodu (`cargo_barcode` = İdefix `cargoKey`) hemen sorulup siparişe yazılır.
- `ship` işleminde İdefix anlaşmalı profil seçilir ve takip no boş bırakılırsa:
  - taşıyıcı bildirilir (`local_status=carrier_set`);
  - İdefix paketi kargoya verince sipariş kendiliğinden "Kargoda" olur.

**Yalnız taşıyıcı bildirilen `ship` (Koçtaş, İdefix, Çiçeksepeti — v1.26.1):** yanıtın `data` alanında `pending: true` ve `status: "CARRIER_SET"` gelir;
sipariş henüz kargoya verilmemiştir. İstemciler "kargoya verildi" yazmamalı, "taşıyıcı bildirildi" yazmalıdır (panel mesajı: "Taşıyıcı bildirildi.
Kargocu paketi okutunca sipariş otomatik 'Kargoda' olur."). Bu durumda webhook olarak `order.updated` (`pending: true`) gider; `order.shipped`
yalnız pazaryeri siparişi gerçekten kargoya verilmiş yaptığında (çekim sonrası, `source: auto`) gider. `POST /orders/bulk` sonuçlarında aynı
bilgi `results[].pending` (+ `results[].note`) olarak döner. `GET /orders/{id}/actions` kayıtlarında `label` bu aşamayı yansıtır ve `phase` alanı
`pending` (taşıyıcı bildirildi) · `cargo_code` (pazaryeri kargo kodu üretti) · `shipped` (pazaryeri kargoya verildi yaptı) · `cancelled` (pazaryeri
iptal etti) değerlerinden birini taşır. Koçtaş bağlantı ayarı `mirakl_ship_mode=now` seçilirse eski davranış (OR24 hemen) sürer.
- `ship` işleminde kendi anlaşmanız seçilir ve takip no girilirse takip linki profilden tamamlanır.

### `POST /orders/{id}/refresh` — durumu pazaryerinden şimdi yenile (v1.23.2)
Siparişin güncel durumu pazaryerinden okunur ve yazılır (Hepsiburada: sipariş + paket detayı; diğerleri: canlı sipariş taraması).
Yanıt: `{"changed":true,"from":"processing","to":"shipped","message":"Durum güncellendi: Hazırlanıyor → Kargoda.","warning":null,"order":{…}}`.
Web / manuel siparişte ve pasif bağlantıda `422`. İşlem geçmişine `refresh` olarak yazılır. Panelde sipariş sayfasındaki "Pazaryerinden yenile".
Hepsiburada'da açık siparişlerin durumu ayrıca kendiliğinden de kontrol edilir (45 dakikada bir, sipariş başına).

### `GET /orders/{id}?raw=1` — detay (+ pazaryerinden gelen ham veri)
### `GET /orders/{id}/actions` — bu sipariş için yapılmış işlemler (geçmiş)
Sipariş kalemlerinde v1.23.0'dan itibaren `product_id` (katalogdaki ürün; ek gönderide kodu değil bunu gönderin), ek gönderide (`marketplace=manual`) siparişte `manual {reason, charge_material, material_cost}`.

### `PUT /orders/{id}` — paket bilgisi / maliyet
`parcel_count` / `desi` değişirse pazaryerine de iletilir (Trendyol box-info, Koçtaş ek alanlar); yanıtta `box_info` (`ok`, `supported`, `note`).
```json
{"parcel_count":2,"desi":12.5,"note":"Kırılacak eşya","cargo_cost":89.90,"commission":null,"local_status":"packed"}
```
`cargo_cost` ve `commission` girilirse finans özetinde bağlantı ayarındaki oranlar yerine bu değerler kullanılır.
Sipariş çıktısında komisyon alanları: `commission` (pazaryerinin bildirdiği tutar; Trendyol kalem oranından hesaplanır, Koçtaş commission_fee + vergiler), `commission_rate` (%), `commission_source` (`marketplace` = pazaryeri bildirdi ya da siparişe elle girildi | `none` = pazaryeri komisyon göndermedi; **tahmin yoktur**),
`commission_estimate` (pazaryeri vermediyse bağlantı ayarındaki oranla tahmin), `net_estimate` (toplam − komisyon − kargo maliyeti).

---

## Sipariş işlemleri

`POST /orders/{id}/{işlem}` — Yanıt `200 {"action":"ship","action_id":77,"data":{…},"order":{…}}`;
pazaryeri kabul etmezse `422 {"error":"action_failed","message":"…"}`. Pazaryeri o işlemi desteklemiyorsa mesajda belirtilir.
Her işlem `order_actions` geçmişine yazılır (`GET /orders/{id}/actions`).

Ortak gövde alanı: `items` — sadece belirli kalemler için işlem yapılacaksa `line_id` listesi (boşsa tüm sipariş).

| İşlem | Gövde | Açıklama |
|---|---|---|
| `accept` | — | Siparişi kabul et (Koçtaş/Mirakl zorunlu; ayar `auto_accept` açıksa otomatik) |
| `pack` | `invoice_number`, `parcel_quantity`, `deci` | Hazırlanıyor / paketle (Trendyol Picking, N11 Picking, HB paket oluşturma, Çiçeksepeti "hazırlanıyor") |
| `ship` | `cargo_company` (ad veya kod), `tracking_number`, `tracking_url`, `shipped_at` | Kargoya verildi + takip no |
| `invoice` | `url` (veya `invoice_url`), `number`, `date`, `pdf_base64` | Fatura linki / dosyası gönder |
| `cancel` | `reason` (`GET /connections/{id}/cancel-reasons`), `note`, `items` | İptal / tedarik edememe (kalem bazlı). Hepsiburada (v1.23.3): Hepsiburada'nın belgelenmiş neden kodları (83 Ürün tedarik edilemedi — varsayılan, 12 Stok yok, 80 Fiyat hatası, 81 Yanlış barkod, 82 Kargo çıkışı yapılamadı, 89 Tedarik gecikmesi, 85, 43, 58, 106 Diğer); eski 1–4 kodları çevrilir. Yalnız paketlenmemiş kalem iptal edilir (paketliyse önce Hepsiburada panelinden paket bozulur) |
| `deliver` | `received_by`, `delivered_at` | Teslim edildi (kendi kargonuzla gönderdiyseniz) |
| `cargo` | `cargo_company` | Kargo firması değiştir. Hepsiburada (v1.23.3): seçilen firma, Hepsiburada'nın o paket / kalem için sunduğu değiştirilebilir firmalar listesinden (`…/changablecargocompanies`) adına göre bulunup kısa adıyla (ShortName) gönderilir; listede yoksa hata metni geçerli seçenekleri sayar. Değişince takip numarası yeniden sorulur |

Yardımcı listeler: `GET /connections/{id}/cargo-companies`, `GET /connections/{id}/cancel-reasons`, `GET /connections/{id}/return-reasons`

Hakediş / ödeme hareketleri (pazaryeri finans servisi, şimdilik Pazarama `paymentAgreement`): `GET /connections/{id}/settlements?from=YYYY-MM-DD&to=YYYY-MM-DD` → `items[]{order_id, order_number, code, amount, commission, coupon, allowance, installments, status, date, transferred_at}`, `totals{amount, commission, allowance}`. Desteklemeyen pazaryerinde `502 marketplace_error`.
(yanıtta ayrıca `supported_actions`).

### Pazaryeri desteği (resmi API'lerin izin verdiği işlemler)

| Pazaryeri | accept | pack | ship | invoice | cancel | deliver | cargo |
|---|---|---|---|---|---|---|---|
| Trendyol | — | ✓ | ✓ | ✓ | ✓ | — | ✓ |
| Hepsiburada | — | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| N11 | — | ✓ | —¹ | ✓ | ✓ | — | — |
| Pazarama | — | ✓ | ✓ | ✓ | ✓ | ✓ | — |
| Çiçeksepeti | — | ✓ | ✓ | ✓² | —³ | ✓ | — |
| İdefix | — | ✓ | ✓ | ✓ | —³ | — | — |
| Koçtaş (Mirakl) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓⁵ | — |
| PttAVM | — | — | —⁴ | ✓ | — | — | — |

¹ N11 API'sinde takip numarası gönderme yok; N11 anlaşmalı kargo etiketiyle çalışır. ² Çiçeksepeti fatura linki değil, fatura e-postası (SendInvoiceMail) kabul eder; `invoice` işlemi o yolu kullanır.
³ Çiçeksepeti ve İdefix satıcı tarafı iptal API'si sağlamaz (pazaryeri panelinden). ⁴ PttAVM kargo bilgisi PTT paneli/kargo entegrasyonundan gelir; API yalnızca fatura yükleme sunar.
⁵ Koçtaş: `ship` işleminde `cargo_company` olarak **`SELLER`** (panelde "Satıcı kendi taşır") seçilirse takip numarası zorunlu değildir ("Kendi Aracımız" yazılır), sipariş "Gönderildi" yapılır ve
Koçtaş ek alanı `shipment-status = Yolda` işlenir; `deliver` işlemi `shipment-status = Tamamlandı` + `delivered-date` gönderir (Yurtiçi hareketlerinden otomatik de tetiklenir).

---

## Fatura — e-Fatura / e-Arşiv (Logo İşbaşı) ve PDF

Sipariş içindeki fatura bilgileriyle (fatura adresi, VKN/TCKN, vergi dairesi — `order.billing_address`) fatura kesilir.
`billing_address.district` faturaya giden **ilçedir** (v1.22.1: Hepsiburada'da ilçe `town` alanından okunur, mahalle adres
satırında kalır; eski kayıtlı siparişlerde ham veriden düzeltilir). Logo İşbaşı ilçeyi tanımazsa ("District Not Found")
düzeltilmiş fatura ilçesi, sonra teslimat ilçesi birer kez denenir; tutan değer faturanın `customer` bilgisine yazılır.
Müşteri GİB'de e-Fatura mükellefiyse e-Fatura, değilse e-Arşiv düzenlenir; GİB'e gönderilir, PDF alınıp barındırılır,
pazaryerine fatura bağlantısı iletilir ve istenirse müşteriye kurumsal şablonla e-postalanır (PDF ekli).

| Uç nokta | Açıklama |
|---|---|
| `POST /orders/{id}/invoice` | `{"send_email":true,"email":"ozel@adres.com","push":true,"sync":true}` → `201 {"invoice":{…}}`. `sync:false` ile kuyruğa alınır (işçi tamamlar) |
| `POST /orders/{id}/invoice/upload` | Hazır fatura: multipart `file` (PDF) veya `{"pdf_base64":"…"}`; `number`, `date`, `send_email`, `email`, `push`. Alternatif `{"url":"https://…/fatura.pdf"}` |
| `GET /orders/{id}/invoice` | Fatura durumu: `status` (queued, processing, created, sent, error, cancelled), `type` (efatura/earsiv/pdf), `number`, `ettn`, `pdf_url` (imzalı, girişsiz), `marketplace_sent_at`, `email_sent_at`, `error` |
| `POST /orders/{id}/invoice/email` | `{"email":"…"}` — faturayı (yeniden) e-posta ile gönder |
| `GET /invoices?status=&order_id=&updated_since=&page=&size=` · `GET /invoices/{id}` | Fatura listesi / detay |
| `POST /invoices/{id}/cancel` · `POST /invoices/{id}/retry` | İptal (e-Arşiv/taslak; ticari e-faturayı alıcı reddeder) · hatalı adımı tekrar dene |
| `GET|PUT /settings/invoicing` | Logo İşbaşı API bilgileri (`isbasi{api_key,username,password,tenant_id}`), SMTP (`smtp{host,port,secure,user,pass,from_email,from_name}`), otomatik kesim (`invoicing.auto_create: never|ship|new`), `auto_push`, `auto_email`, e-posta şablon metinleri. Yanıt: `has_isbasi`, `has_smtp_password` ve gizli olmayan `isbasi{username,tenant_id,has_api_key,has_password}`. Boş gönderilen şifre / anahtar alanı kayıtlı değeri değiştirmez |
| `POST /settings/invoicing/test` | `{"what":"isbasi"}` Logo İşbaşı girişini dener · `{"what":"smtp","to":"…"}` test e-postası gönderir (`to` boşsa hesap e-postası). Başarısızlıkta `422` + hata metni |

---

### Gelen e-Faturalar (Logo İşbaşı gelen kutusu)
| Uç nokta | Açıklama |
|---|---|
| `GET /invoices/incoming?days=30&q=&page=&size=` | Son N günün gelen e-Faturaları (`items[]`, `total`); İşbaşı tanımlı değilse 400 |
| `GET /invoices/incoming/{uuid}/pdf` | PDF (inline) |
| `POST /invoices/incoming/{uuid}/accept` · `…/reject` | `{"note": "…"}` — GİB yanıtı (kabul / red) |

## Toplu işlem

`POST /orders/bulk` `{"ids":[15,16,17],"action":"pack","params":{}}` — `action`: `pack`, `ship` (params: cargo_company, tracking_number…), `ship_create` (params: account_id, parcel_count, desi, phone; sipariş başına ezmek için `rows: {"15": {"parcel_count": 2, "desi": 4.5, "phone": "05xx"}}`), `invoice`, `deliver`, `cancel` (params: reason), `accept`.
Yanıt: `{"done":2,"failed":1,"results":[{"order_id":15,"ok":true},{"order_id":16,"ok":false,"error":"…"}]}`.

---

## Manuel gönderi / yedek parça

`POST /orders/manual` — pazaryerinden bağımsız bir gönderi kaydı açar (`marketplace = "manual"`, sipariş no `M260912-001` biçiminde); isteğe bağlı olarak aynı anda Yurtiçi gönderisi oluşturur.
Tipik kullanım: mevcut bir siparişin alıcısına yedek/eksik parça ya da değişim ürünü göndermek.

```json
{"source_order_id": 12, "reason": "Yedek parça", "parcel_count": 2, "desi": 4,
 "items": [{"sku": "KOLTUK-01", "quantity": 1}, {"name": "Menteşe seti", "quantity": 2}],
 "cargo_account_id": 1, "deduct_stock": false, "note": "Kırılgan"}
```

* `source_order_id` verilirse alıcı ve fatura bilgileri o siparişten kopyalanır; `customer/phone/address/city/district/bill_*` alanlarıyla ezilebilir. Kaynak yoksa `customer`, `address`, `city` zorunludur.
* `items` boş olabilir (ürünsüz gönderi). `product_id` ya da `sku`/barkod ile katalog ürünü, `name` ile serbest kalem. `deduct_stock=true` ise katalog ürünleri stoktan düşülür (kalemde `deduct_stock` varsa o geçerli).
* **v1.23.0 — maliyet:** kalemde `cost` = birim maliyet (₺, KDV dahil; parça gönderiminde parçanın maliyeti). Verilmezse katalogdaki ürün maliyeti. `cargo_cost` = bu gönderinin kargo ücreti (KDV dahil, elle; tarife ezmez), verilmezse desi tarifesi. Malzeme varsayılan olarak gider yazılır (`charge_material:false` ile kapatılır); `material_cost` yalnız elle toplam girilecekse (kalemlerin yerine geçer). Kargo + malzeme **ana siparişin kârından** düşülür.
* **v1.23.0 — kalem atlanmaz:** katalogda bulunmayan `sku`, ad yazılmamışsa kalem adı olur (katalog dışı kalem) ve yanıtta `warnings[]` döner. (Eskiden sessizce atlanıyor, ek gönderi 0 kalemle açılıyordu.)
* Yanıt `201`: `order` (tam sipariş nesnesi; ek gönderide `manual {reason, charge_material, material_cost}`), `shipment` (oluşturulduysa), `shipment_error`, `warnings`, `packing_slip` (HTML/PDF linkleri). Manuel siparişler `GET /orders?marketplace=manual` ile listelenir ve tüm kargo / irsaliye / fatura uç noktalarını kullanır.

**Sonradan düzeltme (v1.23.0, yalnız manuel gönderi):**

| Uç nokta | Açıklama |
|---|---|
| `POST /orders/{id}/items` | Kalem ekle: `{"items":[{"name":"Kapak menteşesi","quantity":2,"cost":45},{"product_id":7,"quantity":1,"deduct_stock":true}]}` — maliyetiyle eklenen kalem malzeme gider yazımını açar. Yanıt `201`: `added, warnings, order, profit` |
| `DELETE /orders/{id}/items/{item_id}` | Kalem çıkar; stoktan düşülmüşse stok geri verilir (`?restore_stock=0` ile verilmez), raf önerisi silinir. Yanıt: `removed, stock_restored, order, profit` |
| `PUT /orders/{id}` `{"charge_material":true,"material_cost":null,"reason":"Hasarlı parça","cargo_cost":120}` | Malzeme gider yazılsın mı, elle toplam (`null` = kalem maliyetleri), gönderim sebebi, kargo ücreti. Pazaryeri siparişinde `charge_material/material_cost/reason` 422 döner |
| `PUT /orders/{id}/items/{item_id}` `{"cost":150,"mode":"order"}` | Kalemin birim maliyeti (malzeme toplamı artık gönderi anında donmaz; düzeltme kâra hemen yansır) |

---

## Kargo — kendi kargo anlaşmanızla gönderi (Yurtiçi Kargo)

Pazaryerinin anlaşmalı kargosu yerine kendi Yurtiçi Kargo sözleşmenizle gönderi oluşturursunuz. Kargo anahtarı
(`prefix` + sipariş numarası) irsaliyeye Code128 barkod olarak basılır; kurye barkodu okutunca Yurtiçi sisteminde takip numarası (docId) oluşur.
İşçi 30 dakikada bir açık gönderileri sorgular, takip numarasını siparişe yazar ve hesap ayarı `auto_notify` açıksa pazaryerine
"kargoya verildi" bildirimini otomatik yapar.

| Uç nokta | Açıklama |
|---|---|
| `GET /cargo/carriers` | Desteklenen kargo firmaları, kimlik alanları ve ayar tanımları |
| `GET /cargo/accounts` | Kargo hesaplarınız |
| `POST /cargo/accounts` | `{"carrier":"yurtici","label":"Ana sözleşme","environment":"prod","credentials":{"username":"…","password":"…"},"settings":{"prefix":"YRK","cargo_count":"1","auto_notify":true}}` → `201` + bağlantı testi sonucu |
| `PUT /cargo/accounts/{id}` | `label`, `environment`, `active`, `settings{}`, `credentials{}` (boş bırakılan kimlik alanı değişmez) |
| `DELETE /cargo/accounts/{id}`, `POST /cargo/accounts/{id}/test` | Sil; bağlantı testi |
| `POST /orders/{id}/shipment` | Sipariş için gönderi: `{"account_id":1,"parcel_count":2,"desi":12.5,"kg":8,"cod_amount":0,"cod_type":"cash","description":"…","cargo_key":"(ops.)"}` → `201 {"shipment":{…}}`. Alıcı bilgileri siparişten alınır |
| `POST /shipments` | Siparişsiz gönderi (WooCommerce/Shopify/kendi sisteminiz): `{"account_id":1,"reference":"WOO-1001","name":"…","address":"…","city":"…","district":"…","phone":"…","email":"…","parcel_count":1,"desi":3,"cod_amount":0}` |
| `GET /shipments?status=created,in_transit&order_id=&cargo_key=&updated_since=&page=&size=` | Gönderiler (durumlar: created, in_transit, delivered, returned, cancelled, error) |
| `GET /shipments/{id}` | Detay: `cargo_key, carrier_key, tracking_number, tracking_url, status, status_text, events[] {date,event,unit,city}, recipient{}`. `carrier_key`: taşıyıcıya gönderilen gerçek anahtar (barkod); hatalı denemede `cargo_key` tablo tekilliği için `#1234` eki alır, `carrier_key` bozulmaz |
| `POST /shipments/{id}/refresh` | Taşıyıcıdan anlık sorgu (takip no / hareketler) |
| `POST /shipments/{id}/cancel` | Gönderiyi iptal et (kargo teslim alınmadan) |
| `POST\|PUT /shipments/{id}/box` | **Koli / desi değiştir** (v1.13.2): `{"parcel_count":3,"desi":18,"kg":0,"description":"","only_record":0}`. Yurtiçi SOAP servisi güncelleme sunmadığı için gönderi **iptal edilip yeni değerlerle yeniden oluşturulur** → yanıt `{shipment{…}, recreated:true, warning}`; **kargo anahtarı ve takip numarası değişir, etiket/irsaliye yeniden basılmalıdır.** Gönderi `in_transit` / `delivered` / `returned` ise ya da `only_record:1` verilmişse taşıyıcıya dokunulmaz: yalnız sipariş kaydı (koli, desi, kargo maliyeti, irsaliye dağılımı) güncellenir, `recreated:false` + uyarı döner. Her iki yolda da pazaryeri destekliyorsa yeni koli/desi pazaryerine bildirilir. İptal başarılıysa ama yeniden oluşturma başarısızsa hata metni bunu açıkça söyler (gönderi iptal kalır, Kargo kartından yeniden denenir). |

**Takip sıklığı (v1.13.2):** gönderiler kademeli sorgulanır — ilk **48 saat 5 dakikada bir**, sonrasında **30 dakikada bir**. Kargo hesabı ayarlarından değiştirilir: `track_fast_minutes` (5), `track_slow_minutes` (30), `track_fresh_hours` (48).

---

## İrsaliye (sevk fişi)

| Uç nokta | Açıklama |
|---|---|
| `GET /orders/{id}/packing-slip` | Yazdırmaya hazır HTML (A4): logo, gönderici, **fatura bilgileri (firma adı, VKN/TCKN, vergi dairesi)**, teslimat adresi, ürün görselleri, KDV, toplam, büyük koli sayısı kutusu, kargo barkodu |
| `GET /orders/{id}/packing-slip.pdf` | PDF (sunucuda wkhtmltopdf varsa; yoksa `501`) |
| `GET /orders/{id}/packing-slip?json=1` | Model + HTML (kendi şablonunuzu basmak için) |
| Parametreler | `show_images=0`, `show_prices=0`, `show_barcode=0` bu çıktı için ayarı geçersiz kılar |
| `show_customer=0\|1`, `hidden_show_city=0\|1` (v1.26.0) | Müşteri bilgilerini **bu baskıda** gizle / göster (öncelik: istek > anahtar profili > hesap profili > varsayılan açık). Gizliyken ad, firma, adres, telefon, e-posta ve VKN/TCKN irsaliyeye, etiketlere ve `json=1` çıktısındaki `ship_to` / `bill_to`'ya yazılmaz; fatura kutusunda yalnız "Bireysel / Kurumsal müşteri", teslimat kutusunda "Alıcı bilgileri kargo barkodunda — barkodu okutun" yazar ve barkod her zaman basılır. Siparişin gerçek kargo barkodu yoksa (barkod sipariş numarasına düşüyorsa) bilgiler **gizlenmez**; modelde `customer_hide_skipped:true`. Toplu çağrıda sipariş başına `items[].show_customer` da verilebilir |
| *(biçim verilmezse)* | **Siparişte kayıtlı dağılım** basılır (`PUT /orders/{id}` ile girilen `slip_detailed` / `slip_labels`); kaydedilmemişse 1 detaylı + kalan koliler etiket |
| `mode=full\|detailed\|label\|mixed\|both`, `detailed=N`, `labels=M` | Koli başına sayfa türü — verilirse kayıtlı dağılımı ezer: `full` / `detailed` hepsi detaylı, `label` hepsi sadece barkod etiketi, `mixed` ilk koli detaylı kalanı etiket, `both` her koliye hem detaylı hem etiket; `detailed`/`labels` sayı verilirse doğrudan o kadar basılır (örn. 12 koli `detailed=3&labels=9`). Etiketler A4'e 8 adet, k / N koli numarasıyla |
| `GET /orders/packing-slips?ids=1,2,3&mode=label` | Birden çok siparişin irsaliyesi tek belgede: detaylı sayfalar sipariş sırasıyla, tüm etiketler sona toplu. `.pdf` eki ya da `format=pdf` ile PDF |
| `POST /orders/packing-slips` | `{"items":[{"id":15,"detailed":3,"labels":9},{"id":16,"mode":"label"}],"format":"pdf"}` — sipariş başına farklı sayı |
| `profile=key` | İrsaliye, **isteği yapan API anahtarının** gönderici profili ve logosuyla basılır (sipariş hangi kanaldan gelirse gelsin). Aynı hesaba bağlı her site kendi firma bilgisiyle bassın diye; anahtar profilinde boş bırakılan alanlar hesap profilinden devralınır. Ana anahtarla istenirse hesap profili kullanılır. `GET /orders/{id}/packing-slip` ve `GET /orders/packing-slips` için geçerlidir |

Barkod içeriği: kendi kargo gönderisi varsa **kargo anahtarı**, yoksa pazaryeri takip numarası, o da yoksa sipariş numarası.
Barkod altındaki etiket siparişin geldiği pazaryerini belirtir: `Yurtiçi Kargo Marketplace - Trendyol` (takip no varsa ayrıca yazılır).
Koli sayısı kadar sayfa üretilir; her sayfada barkodun altında büyük **`k / N`** (ör. `2 / 3`) yazar — her koliye bir irsaliye yapıştırılır.
Gönderici bilgileri, logo ve varsayılan görünüm `PUT /profile` ile (veya panel › Ayarlar) belirlenir.

### `GET /profile` · `PUT /profile`
Hesap profili. **Siteye özel profil:** ek API anahtarıyla (Panel › API › ek anahtar, ör. kanal `woocommerce`) `GET /profile?scope=key` o anahtarın profilini (`key{id,label,channel,override,profile}`) ve etkin profili (`profile`) verir; `PUT /profile` gövdesine `"scope":"key"` eklenirse yalnız o anahtarın profili yazılır (`"clear":true` kaldırır). Anahtar profili, o anahtarla açılan **web / manuel siparişlerin** irsaliyesinde hesap profilini ezer (dolu alanlar); pazaryeri siparişleri her zaman hesap profilini kullanır. Böylece aynı hesaba bağlı birden çok site kendi gönderici bilgisi ve logosuyla irsaliye basar.
```json
{"company":"Örnek Mobilya","address":"…","district":"Konak","city":"İzmir","postal_code":"35160","phone":"…","email":"…","website":"https://…",
 "tax_office":"Konak","tax_number":"1234567890","logo":"data:image/png;base64,…",
 "slip":{"title":"İrsaliye","accent":"#f26522","note":"İade için 14 gün…","show_images":true,"show_prices":true,"show_barcode":true,"show_customer_phone":true,
         "show_customer":true,"hidden_show_city":false},
 "orders":{"ship_warn_hours":24}}
```
Anahtar profilinde **boş bırakılan metin alanları hesap profilinden devralınır** (irsaliye başlığı dâhil); onay kutuları her zaman anahtarın değerini kullanır. Yanıt yalnız gönderici / irsaliye alanlarını içerir — SMTP, Logo İşbaşı kimlikleri, bildirim tercihleri dönmez.

---

## Finans

| Uç nokta | Açıklama |
|---|---|
| `GET /finance/summary?since=2026-09-01&until=2026-09-30` | `revenue, orders, items, avg_order, cancelled, commission, cargo, expenses, net, by_marketplace[] {marketplace, orders, revenue, commission, cargo, cancelled, net}, by_day[] {date, orders, revenue}, expenses_by_category[]` — ayrıntılı alanlar aşağıda "Kârlılık" bölümünde; v1.23.0: `checks[]` (eksik ayar / veri uyarıları) |
| `GET /finance/orders?since&until&marketplace&filter&q&sort&dir&page&per_page` | **v1.23.0** Sipariş bazında kârlılık (iade/iptal ve ek gönderiler sonrası). Satır: `id, order_number, marketplace, order_date, status, customer, quantity, sale (satış), revenue (iade sonrası ciro), reversed, commission, fees, stopaj, payout, cargo (gidiş), extra_cargo (ek gönderi), return_cargo (iade dönüşü), cogs (satılan), extra_cogs (ek gönderi malzemesi), cogs_lost (hasarlı iade), pretax, vat_payable, tax_due, net, margin, cost_missing, cargo_known, extras, extras_total, reversal, reversal_loss`. `filter`: boş (masrafsız iptaller gizli) · `all` · `loss` (net < 0) · `cost_missing` · `extras` · `returns` · `cargo_missing`. `sort`: `date|pretax|net|margin|revenue|extras_total|cogs`, `dir`: `asc|desc`. `per_page=0` → hepsi (dışa aktarma). Yanıtta `totals` |
| `GET /finance/products?since&until&marketplace&q&sort&dir&page&per_page` | **v1.23.0** Ürün bazında kârlılık: `product_id, sku, name, image, orders, quantity, returned, return_rate, net_quantity, sale, revenue, commission, fees (kesinti + stopaj), cargo (gidiş + iade dönüşü payı), extras (ek gönderi payı), extras_orders, cogs, pretax, margin, unit_profit, avg_price, unit_cost, cost_missing`. Kaleme ait olanlar (satış, iade edilen adet, maliyet, hasarlı iade) kalemden; siparişe ait olanlar (komisyon, kesinti, kargo, ek gönderi) kalemin ciro payıyla dağıtılır — kalemlerin kârı toplamı siparişin kârına eşittir. İptal edilen sipariş adet olarak sayılmaz. `sort`: `pretax|revenue|quantity|margin|returns|extras|unit_profit` |
| `GET /finance/extras?since&until&marketplace&filter=cost_missing&q&page&per_page` | **v1.23.0** Ek gönderiler (yedek parça / eksik parça / değişim): ana siparişin tarihine göre dönemdekiler + bağımsız manuel gönderiler. Satır: `id, order_number, date, reason, status, parent {id, order_number, marketplace, order_date, customer}, lines[] {name, sku, product_id, quantity, unit_cost}, cargo, cargo_source, cargo_known, material, charge_material, material_override, cost_missing, total`. Yanıtta `totals`, `by_reason[]`, `by_product[]` (ana siparişteki ürüne göre — hangi üründe ek gönderi çıkıyor) |
| `GET /finance/monthly?months=12&until=2026-09` | **v1.23.0** Aylık seyir: her ay `month, label, orders, revenue, payout, pretax_profit, expenses, operating_profit, tax_total, net, margin, net_margin, returns_loss, extras_total, cost_missing, cancelled` |
| `GET /expenses?since=&until=` | Giderler (`vat_rate`, `vat`, `recurring_id`, `period` ile); yanıtta `categories`, `vat_defaults` |
| `POST /expenses` | `{"date":"2026-09-11","category":"ads","amount":1500,"vat_rate":20,"note":"Eylül reklam","marketplace":"trendyol"}` — tutar KDV dahil; `vat_rate` verilmezse kategori varsayılanı (personel, vergi %0; diğerleri %20). Kategoriler: cargo, commission, ads, packaging, purchase, salary, rent, tax, software, accounting, utilities, vehicle, affiliate (satış ortaklığı — siparişe bağlanmayan), payment (ödeme / POS — siparişe bağlanmayan), other |
| `PUT /expenses/{id}` · `DELETE /expenses/{id}` | Düzelt (yalnız verilen alanlar) / sil |
| `PUT /expenses/by-key` | **v1.23.0** Anahtarlı gider — bağlı sistemin (site) aynı gideri tekrar tekrar güncellemesi için: `{"ext_key":"site:affiliate:2026-09","date":"2026-09-28","category":"affiliate","amount":120,"vat_rate":0,"note":"…"}`. Aynı `ext_key` varsa o kayıt güncellenir (200), yoksa eklenir (201, `created:true`); `amount` 0 → kayıt silinir (`deleted:true`). `ext_key`: harf, rakam, `_ . : -` (en çok 80). Giderde `ext_key` alanı. e-ticaret sitesi 1.11.0 siparişe bağlanmayan nakit satış ortaklığı ücretlerini (tıklama başı, elle düzeltme) ay başına bu uçla yazar |
| `GET /expenses/recurring` · `POST /expenses/recurring` · `PUT|DELETE /expenses/recurring/{id}` | **v1.23.0** Tekrarlayan giderler: `{"category":"rent","amount":10000,"vat_rate":20,"day":1,"start_month":"2026-06","end_month":null,"note":"Depo kirası","marketplace":null,"active":true}` — her ay `day`. gününde gider kaydı yazılır (saatlik iş + finans ekranı açılışı; aynı ay bir kez; yazılmış ayın kaydı silinirse yeniden yazılmaz). Düzeltme yalnız sonraki ayları etkiler; şablon silinince yazılmış kayıtlar kalır. `day` 1–28 |

**v1.23.0 — işletme giderleri vergiden önce:** giderin KDV'si indirilecek KDV'ye, tutarı kâr vergisi matrahına girer (`opex`, `opex_vat`, `operating_profit = pretax_profit − expenses`, `tax_total = vat_payable + tax_due`, `net = operating_profit − vat_payable − tax_due`). Pazaryeri seçilmiş gider (ör. reklam) `by_marketplace[]` satırında da o pazaryerinden düşülür (`expenses`). Ek gönderiler özet alanları: `extras_count, extras_total, extras_cost_missing, extra_cargo, extra_cogs`; ayrıca `cargo_unknown` (kargo maliyeti bilinmeyen sipariş), `loss_orders`, `returns_count`, `cancels_count`.

Komisyon = sipariş tutarı × bağlantı ayarındaki oran (veya siparişe `PUT /orders/{id}` ile girilen `commission`);
kargo = siparişe girilen `cargo_cost` › gönderi maliyeti › bağlantı ayarındaki ortalama; iptal/iade siparişler ciroya dahil edilmez.

---

## İadeler (iade / değişim / iptal talepleri)

Talepler her bağlantı için periyodik çekilir (`sync_returns` ayarı).

### `GET /returns`
Parametreler: `status` (`requested,review,approved,rejected,in_transit,received,completed,cancelled`), `type` (`return|change|cancel`), `marketplace`,
`updated_since`, `order_number`, `return_code` (v1.27.0: iade kodu, ör. `YRKN000002`), `page`, `size`, `include=items`.

### `GET /returns/{id}?raw=1` — detay (kalemler, neden, müşteri, kargo)
`items[]`: `id, line_id, order_line_id, order_item_id, product_id, catalog_sku` (v1.23.3: bağlı katalog ürününün kodu — pazaryeri iade satırında kendi kodunu verebilir, ör. Hepsiburada `HBV…`)`, name, sku, barcode, image, quantity, amount, reason, status, restock_qty` (karara bağlanan adet),
`restocked_ok` (stoğa giren adet), `restock_remaining` (karar bekleyen adet), `restock_state` (`''` bekliyor · `partial` · `ok` · `damaged` · `mixed` · `none` (v1.23.11: stok değişmesin)), `restock_note`, `restocked_at`.
v1.23.11: raflama açıksa karar bekleyen kalemde `suggestion` (önerilen raf: `{code, location_id, reason, label, quantity, alternatives[]}` — bkz. Raflama › Ürün nerede?); iadede `restock_due_at` (stok kararının beklenmeye başladığı an), `restock_pending` ("Stok kararı bekleyen" listesinde mi), `restock_age_days` (kaç gündür).
Pazaryeri iade satırında ürün vermese bile (Koçtaş, Hepsiburada, Trendyol çoğu zaman sadece sipariş kalem kimliği verir) kalem sipariş kaleminden tamamlanır: ad, SKU, barkod, katalog ürünü, görsel.

### `POST /returns/{id}/{karar}` — `approve` | `reject` | `receive` | `pending`
Gövde: `{"reason":"…","note":"…","items":["line_id"],"days":3,"restock":"yes|no"}`. Yanıt `{"decision":"approve","data":{…},"return":{…}}`.
`restock`: ürün elinize ulaşınca stoğa eklensin mi (`yes` sağlam / `no` hasarlı). Verilmezse ürün geri geldiğinde (`received`/`completed`) `return.restock` boş kalır ve panelde "stoğa girsin mi?" sorulur.

**`POST /returns/{id}/receive` — "Teslim aldım" (v1.26.0).** Gövde (hepsi isteğe bağlı): `{"restock":"yes|no","notify":true|false,"message":"…","by":"Depo"}`.
- Teslim alma **her zaman burada kaydedilir** (`received_at`, `received_by`) — pazaryeri kabul etmese de; stok kararı (`restock`) eskisi gibi uygulanır.
- Pazaryerinin kendi "teslim alındı" ucu varsa denenir (Koçtaş RT25, yalnız gerçek iade kimliğinde) → `receipt_sync: "sent"`.
- Koçtaş iadeyi kendisi yönetiyorsa / iade modülü kapalıysa / kayıt sipariş hareketinden (refund `rf:`, şikâyet `inc:`) türediyse ve `notify` doğruysa
  (verilmezse bağlantı ayarı "İade teslim alınınca Koçtaş operatörüne mesaj gönder", varsayılan açık) sipariş üzerinden operatöre mesaj yazılır:
  varsa siparişin operatörlü görüşmesine (M12), yoksa yeni sipariş görüşmesi (OR43) → `receipt_sync: "message_sent"`, `receipt_ref: "M12:{thread}/{message}"`.
  `message` boşsa bağlantı ayarındaki şablon (`{siparis_no}`, `{urun}`, `{adet}`, `{tarih}`) doldurulur; `return.receipt_message` önceden doldurulmuş metni verir.
- Bildirim istenmediyse / yapılamıyorsa `receipt_sync: "local"` (+ `receipt_error`: pazaryerinin gerekçesi, Türkçe); mesaj gönderilemediyse `"failed"`.
- İptal kayıtlarında (`type: cancel`) mesaj gönderilmez (`can_notify: false`).
Yanıt: `{"ok":true,"decision":"receive","receipt_sync":"sent|message_sent|local|failed","note":"…","error":null|"…","data":{…},"return":{…}}`.
`publicReturn` yeni alanlar: `received_at`, `received_by`, `receipt_sync`, `receipt_error`, `receipt_ref`, `receipt_message`, `can_notify`.

### `POST /returns/{id}/restock` — kalem bazlı, adetli stok kararı
```json
{"items":[{"id":12,"quantity":2,"state":"ok","note":"2'si sağlam"},{"id":12,"quantity":1,"state":"damaged"},{"line_id":"ci-9","state":"skip"}],"note":"…"}
```
`state`: `ok` → adet kadar katalog stoğuna eklenir ve tüm pazaryerlerine yayılır (olay kaynağı `{pazaryeri}_iade`); `damaged` → stoğa girmez, işaretlenir; `none` (v1.23.11) → **stok ve raf değişmez**, kalem kapanır (ürün elinize geçmedi — kayıp kargo, ürünsüz para iadesi — ya da stok daha önce elle düzeltildi; "hasarlı" sayılmaz); `skip` → sonra. Tanınmayan `state` artık `ok` sayılmaz: `422` (v1.23.11).
Raflama açıksa `location` (kalemde ya da üstte): raf kodu, `RAFSIZ` ya da `auto` (v1.23.11: ürünün önerilen rafı — durduğu raf → bu siparişte toplandığı raf → en son rafı → çeşit / kategori rafı; hiçbiri yoksa rafsız). Sağlam ürün o rafa fiziksel giriş yapar (`return_in` hareketi, `stock.moved` olayı). `location` boşsa ürünün sipariş için toplandığı raf, o da yoksa `RAFSIZ` kullanılır; yanıttaki `items[].location` nereye alındığını söyler.
`quantity` verilmezse kalemin kalan adedi; kalanı aşan adet kırpılır; kısmi girişler birikir (`restock_qty`, `restocked_ok`). `items` verilmezse `{"state":"ok"|"damaged"}` tüm kalemlere uygulanır (eski `{"restock":true|false}` de kabul edilir).
Sipariş iptalinde stok zaten geri verilmiş kaleme (order_items.applied = 2) tekrar eklenmez, uyarı döner; siparişe bağsız kalem önce siparişin kalemine bağlanmaya çalışılır (sipariş numarası + SKU/barkod). Yanıt: `{"restocked":2,"done":true|false,"items":[{"id","sku","product_id","state","quantity","restocked","warning"}],"return":{…}}`.
**Kapı (panel ile aynı):** stok kararı zaten verilmiş (`restocked_at` dolu) ya da durumu `approved` / `in_transit` / `received` / `completed` olmayan iadede istek `action_failed` (422) ile reddedilir; `message` Türkçe gerekçedir ("Bu iade "Reddedildi" durumunda — ürün geri gelmeden stoğa alınamaz…"). İstisna: sipariş kalemi bulunamayan `type = cancel` kayıtlar (karar sahibinin).
`type = cancel` kayıtlarda (ürün müşteriye gitmemiş iptal) stok **sipariş kaleminden** geri verilir: iptal kesinleşince bağlı sipariş kalemi bulunursa kalem hemen iptal edilir ve stok bir kez geri gelir (kayıt `restock: no` ile kapanır); sipariş bulunamazsa kayıt açık kalır ve panelde "İptal — sipariş kalemi bulunamadı, stok kararı sizde" diye sorulur. Onaylandı'da kalan (pazaryerinin "ürün ulaştı" demediği) iadeler onaydan 3 gün sonra "stoğa girsin mi?" listesine düşer.

### `GET /returns/pending-restock?limit=200` — stok kararı bekleyen iadeler (v1.23.11)
Ürünü geri gelmiş (`received` / `completed`, `cancel` kayıtlarda karar sahibine kalan) ya da onaylanıp 3 gün geçmiş, stok kararı verilmemiş iadeler, **en eskiden** (`restock_due_at`).
Yanıt: `summary{count, web, total, overdue, oldest_days, oldest_at}` (`count`: kararı burada verilecek pazaryeri iadeleri; `overdue`: 2 günü geçen; `web`: kararı web sitesinde verilen web iadesi), `returns_open` (onay / ret bekleyen talep sayısı), `items[]`: `GET /returns/{id}` biçiminde iade + kalemler (kalemde `suggestion`) ve `due_since`, `age_days`, `pending_note` (neden soruluyor), `site_managed` (web iadesi), `can_restock`, `actions[]` (hızlı karar: `ok`, `damaged`, `none`; web iadesinde yalnız `none`).
`?summary=1`: yalnız `summary` ve `returns_open` (menü sayısı / uyarı için hafif çağrı).

### `POST /returns/restock-bulk` — hızlı / toplu stok kararı (v1.23.11)
```json
{"ids":[12,13],"state":"ok","location":"auto","note":""}
```
Seçilen iadelerin **kalan bütün kalemlerine** tek karar: `state` `ok` | `damaged` | `none`; `location` `auto` (varsayılan; kalem başına önerilen raf) | `RAFSIZ` | raf kodu. Her iade `restock` ucunun kapısından geçer (kararı verilmiş / ürünü gelmemiş iade atlanır, gerekçesiyle). Katalogda karşılığı olmayan kalemde `ok` reddedilir (stok eklenmeden kapanırdı); kalem bilgisi olmayan iade yalnız `none` ile kapanır; web sitesi iadesinde yalnız `none` (kararı sitenin teslim alma ekranında verilir).
Yanıt: `{"done":1,"failed":1,"restocked":2,"results":[{"id":12,"ok":true,"label":"Trendyol #…","error":null,"restocked":2,"done":true,"items":[…]},{"id":13,"ok":false,"error":"…"}],"summary":{…}}`.

### `POST /orders/{id}/reactivate` — iptalden çıkarma
Web sitesi iptal edilmiş siparişi yeniden aktif ederse çağrılır. İptal edilmiş kalemler açılır, iptalde geri verilen stok **tekrar düşülür** (olay kaynağı `{kanal}_yeniden`), sipariş `processing` olur. İkinci çağrı ve iptal edilmemiş siparişte no-op (`data.reactivated: 0`). Yanıt: `{"action":"reactivate","data":{"reactivated":1,"deducted":2,"note":"…"},"order":{…}}`.

| Pazaryeri | Talep listesi | Onay | Ret | Teslim aldım |
|---|---|---|---|---|
| Trendyol (claims) | ✓ | ✓ | ✓ (itiraz + dosya) | — |
| Hepsiburada (claims) | ✓ | ✓ | ✓ | ✓ (ön onay) |
| N11 (ReturnService) | ✓ | ✓ | ✓ | ✓ (beklet) |
| Pazarama (refund) | ✓ | ✓ | ✓ | — |
| Çiçeksepeti (cancel evaluation) | ✓ | ✓ | ✓ | — |
| İdefix (claim) | ✓ | ✓ | — | — |
| Koçtaş (RT + OR11 refund/iptal/şikâyet) | ✓ | ✓ (yalnız RT) | ✓ (yalnız RT) | ✓ (v1.26.0: RT25; operatör yönetiyorsa / rf: inc: kaydıysa operatöre mesaj) |
| PttAVM | — | — | — | — |

### İade kodları — Yurtiçi'de otomatik takip (v1.27.0; ortak sıra v1.28.0)

Web sitesi iade onaylayınca müşteriye bir **iade kodu** verir (`YRKN000002`, `YRKN000003`, … — sıra sitede artar). Müşteri paketi
Yurtiçi şubesinden gönderirken kargo fişine / gönderi referansına **"Sabit Cari 990148039 - YRKN000002"** yazdırır (adres yok:
alıcı Yurtiçi'de bu **sabit cari** hesabınızdır). Pazaryeri Merkezi **Yurtiçi'nin müşteri rapor servisini** (`WsReportWithReferenceServices`)
periyodik tarar, kodu taşıyan gönderiyi bulur, **takip numarasını / bağlantısını / durumu** çeker ve iadeye yazar.
Ayrıntılı tasarım: `IADE-KODU.md`; Yurtiçi servis notları: `api-ref/yurtici-rapor.md`.

**Önkoşul:** Kargo sayfasında Yurtiçi hesabı + panel › **İade kodları** sayfasında izleme açık, kod ön eki (varsayılan `YRKN`) ve sabit cari
numarası girili. Hesap yoksa kod yine iadede durur; izleme açılınca eski kodlar kendiliğinden kayıt defterine alınır.

| Uç | Açıklama |
|---|---|
| `POST /returns` (+ `return_code`) | Site iadeyi bildirirken kodu da yollar. Kod `returns.return_code`'a yazılır ve izlemeye alınır. Yanıt/okumada: `return_code`, `tracking_number`, `tracking_url`, `cargo_status` (`in_transit` · `delivered` …), `cargo_status_text`, `cargo_delivered_at`. |
| `GET /returns?return_code=YRKN000002` | Koda göre iade. |
| `GET /return-codes` | İzlenen kodlar. Süzgeç: `state` (`waiting,transit,delivered,cancelled,stopped,expired`), `linked=1\|0`, `source` (`site,wp,api,manual,scan`; virgülle çoklu), `q` (kod, takip numarası ve **not**), `updated_since`, `page`, `size`. 1.28.0: her `item`'da bağlı iade özeti `return_ref{id,number,order_number,marketplace,status,customer}` ve `note`. |
| `POST /return-codes` | `{"code":"YRKN000001","return_id":12,"lookup":true,"source":"manual","note":"…","claim":false}` — kodu izlemeye al / iadeye bağla / Yurtiçi'de hemen ara. 1.28.0: `source` (`site\|wp\|api\|manual`, varsayılan `api`) ve `note` (≤250 karakter) kodu ekleyen kanalı yazar; `claim:true` → kod zaten kayıtlıysa, iadeye bağlı değilse ve kaynağı belirsizse (`scan\|manual\|api`) kaynak / not isteyen kanala geçer (WordPress'in elle verilmiş kodu bir talebe bağlaması) ve ortak sıra o numaranın ötesine ilerler. Yanıt: `found`, `item`, `diag[]` (adım adım tanılama), `near[]`, `bind_error`. Sitenin başka kod verdiği web iadesine farklı kod bağlanamaz → `409 conflict`. |
| `POST /return-codes/allocate` | **v1.28.0 — ORTAK SIRADAN sıradaki kodu verir** ve kayıt defterine yazar: `{"source":"site\|wp\|api","note":"WordPress · IADE2611070001 · sipariş #1001","return_id":0,"floor":0}`. Site ve WordPress aynı Yurtiçi carisini kullandığı için kod buradan alınır; iki kanal aynı numarayı veremez (benzersiz dizin + çakışmada sonraki numara). Aynı `return_id` yeniden sorarsa **mevcut kod** döner (idempotent, `created:false`, HTTP 200); yeni kod 201. `floor` istemcinin bildiği alt sınırdır. Numara: `max(ayardaki "Sıradaki numara", floor, site / WordPress / API'nin verdiği en yüksek numara + 1)`; Yurtiçi'de keşfedilen ve elle yazılan kodlar sırayı atlatmaz, ama dolu numaralar atlanır. Yanıt: `{code, created, item, config{prefix,digits,cari,next}}`. Etkin Yurtiçi hesabı / açık izleme yoksa `422 action_failed`; `return_id` bulunamazsa `404`. |
| `GET /return-codes/config` | **v1.28.0** — Site / WordPress için ortak ayar özeti (ağa çıkmaz): `ready, enabled, prefix, digits, cari, cari_list[], cari_source` (`settings` \| `account` = ayarda boşsa Yurtiçi hesabının kendi müşteri numarası), `next, next_number, account{id,label,environment}, reason`. |
| `GET /return-codes/{code}` | Tek kod + bağlı iade. `{code}` esnek yazılabilir (`yrkn2`, `YRKN2`, `YRKN000002` aynı kodu bulur). |
| `POST /return-codes/{code}/refresh` | Yurtiçi'de şimdi ara (tanılamalı). |
| `POST /return-codes/{code}/stop` · `/resume` | İzlemeyi durdur / yeniden başlat (60 günü aşıp hiç görülmeyen kod `expired` olur; `resume` süreyi sıfırlar). |
| `DELETE /return-codes/{code}` | Kaydı sil (iadedeki kod alanı kalır). |
| `POST /return-codes/scan` | İzleme açık hesapları şimdi tara. |

`item` alanları: `code, state, state_label, tracking_number, tracking_url, parcel_count, parcels[{tracking_number, tracking_url, status, status_text, created_at, delivered_at}],
status_text, cargo_status, cargo_status_label, seen_at, delivered_at, last_check_at, checks, last_error, return_id, source (site|wp|api|manual|scan), note, return_ref, sender_hint (kısaltılmış), sender_city, match_field`.

**Webhook:** takip no / durum değişince bağlı iade için `return.updated` (`reason: "return_code"`) gider; yalnız değişiklikte, döngü olmaz. Bildirim olayı: `return.cargo`
(bildirim türü ayarlanabilir). **Gizlilik:** Yurtiçi'den gelen alıcı / gönderici adı, adres ve telefon saklanmaz; tanılama örneklerinde de görünmez (gönderici adı yalnız kısaltılmış ipucu).

---

## Raflama — depo yerleşimi (raflar, fiziksel stok, toplama)

Hesap başına açılıp kapanan özellik (`PUT /settings/features {"shelves": true}` ya da Ayarlar › Özellikler). Kapalıyken hiçbir davranış değişmez; `/locations`, `/stock/*` ve `/orders/{id}/pick` uçları `400 feature_disabled` döner.

**İki stok kavramı**

| | Nedir | Ne zaman değişir |
|---|---|---|
| Satılabilir stok (`stock`) | Pazaryerlerine gönderilen adet (`GET /products`, `POST /stock`) | Sipariş gelince düşer, iptal / iade ile geri gelir; mal girişi / sarf / kayıp / sayım ile değişir |
| Fiziksel stok (`shelves.physical`) | Raflardaki adetlerin toplamı; `RAFSIZ` = henüz rafa yerleşmemiş kova | Sipariş **toplanınca** (pick) düşer; mal girişi, transfer, sarf, kayıp, sayım, iade girişi ile değişir |

`fiziksel − toplanmayı bekleyen = satılabilir` beklenir; `difference` alanı sapmayı verir. v1.22.1'den beri toplanmayı bekleyenin yalnız **satılabilirden düşülmüş** kısmı sayılır (`pending_deducted`; stoktan düşülmemiş geçmiş / "düşüm kapalı" sipariş kaleminin adedi zaten satılabilirin içindedir): `difference = physical − pending_deducted − stock`. **Depoda olan her ürün satışa açıktır** (v1.22.1): satılabilir stok yazımları depo kaydını da taşır (`POST /stock` › `depo`), sapma ürün sayfasındaki "Depodakini satışa aç" / "Satılabilir doğru → depo kaydını düzelt" ile tek tıkla kapatılır. Özellik açıldığında raf kaydı olmayan ürünlerin satılabilir stoğu `RAFSIZ` olarak yazılır (`init` hareketi); yeni ürünlerde de ilk stok rafsız başlar.

**Sipariş akışı**: sipariş gelir → kataloğa bağlı her kaleme raf **önerisi** yazılır (`items[].locations`, `status: suggested`; strateji: `most` en çok stoklu raf, `least` en az stoklu, `code` raf sırası; rafın müsait adedi = raf stoğu − diğer açık siparişlerin önerileri; raflar yetmezse `RAFSIZ`) → irsaliyede görselin altına raf adresleri basılır (`R00-001 ×1`, `R00-002 ×2`) → depo toplar ve **toplama onaylanır** (`POST /orders/{id}/pick`; önerilen raftan ya da başka raftan, kısmi olabilir) → o rafın fiziksel stoğu düşer (`pick` hareketi). Paket kontrolde barkod okutulunca (`POST /orders/pack-check`) ve kargoya verildiğinde toplanmamışsa öneri otomatik uygulanır (ayarlar `shelves_pick_on_check`, `shelves_auto_pick`). Toplanmamış kalem iptal edilirse öneri silinir (raf değişmez); toplanmış kalem kargoya verilmeden iptal edilirse ürün **alındığı rafa** geri yazılır (`cancel_restore`; raf silinmişse `RAFSIZ`); kargoya verilmiş siparişin ürünü iade akışıyla döner: "stoğa girsin mi?" sorulur, sağlam adet `POST /returns/{id}/restock` ile seçilen rafa girer (`return_in`; raf verilmezse ürünün toplandığı raf, o da yoksa `RAFSIZ`).

### Ayarlar
| Uç nokta | Açıklama |
|---|---|
| `GET /settings/features` | `features{shelves, shelves_strategy (most\|least\|code), shelves_auto_pick, shelves_pick_on_check, zero_on_reject, shelves_stock_sync}`, `strategies`. `shelves_stock_sync` (v1.22.1, varsayılan açık): satılabilir stok yazımı depo kaydını da taşır. |
| `PUT /settings/features` | Aynı alanlar (yalnız gönderilenler değişir). Raflama ilk açılışta `initialized{products, orders}` döner. `zero_on_reject`: panel / API'den "İptal / tedarik edememe" (`POST /orders/{id}/cancel`) yapıldığında o ürünlerin satılabilir stoğu 0 yapılır ve tüm pazaryerlerine 0 gönderilir (olay `red_sifirla`, tip `reset`); müşteri / pazaryeri iptallerinde uygulanmaz. |

### Raflar
| Uç nokta | Açıklama |
|---|---|
| `GET /locations?active=1` | `items[] {id, code, name, zone, note, is_active, sort, stats{products, quantity, pending_pick}}`; `unassigned: "RAFSIZ"` |
| `POST /locations` | Tek: `{"code":"R00-001","name":"Depo A · 1. koridor","zone":"A","note":"","sort":10,"is_active":true}` → `location`. Toplu: `{"codes":["R00-001","R00-002"],"zone":"A"}` → `created, skipped[] (zaten var), errors[]`. Kod: harf/rakamla başlar, A-Z 0-9 `- _ . /`, ≤40; küçük harf büyütülür, boşluk `-` olur; `RAFSIZ` ayrılmıştır. |
| `GET /locations/{id\|kod}` | Raf + `stock[] {product_id, sku, barcode, title, quantity, pending_pick, available, sellable}` |
| `GET /locations/{id\|kod}/stock` | Yalnız içerik |
| `PUT /locations/{id\|kod}` | `{"code","name","zone","note","sort","is_active"}` (pasif raf öneriye girmez, stoğu görünür) |
| `DELETE /locations/{id\|kod}?move_to=RAFSIZ\|R00-002` | Rafta stok varsa `move_to` zorunlu (içerik taşınır: `transfer` hareketi); yanıt `moved`. Bekleyen öneriler `RAFSIZ`a düşer ve yeniden hesaplanır. |

### Raf stokları
| Uç nokta | Açıklama |
|---|---|
| `POST /stock/sync-physical` | v1.22.1: depo (fiziksel) kaydını satılabilire eşitler — `{"sku":"…","note":"…","expected_difference":4}` ya da `{"items":[…]}`. Ürün kilitli, göreli hareketler (`adjust`, `sellable_after: null`, `ref: "stok-yazimi-depo"`). `expected_difference` (fiziksel − düşülmüş toplanacak − satılabilir) farklıysa `changed: true`, yazılmaz. Yanıt: `{done, failed, results[{sku, ok, changed, difference, moved, left, before, after, movements[], product}]}` |
| `GET /stock/locations?sku=\|product_id=` | Tek ürünün dökümü: `product{sku, sellable, physical, unassigned, pending_pick, pending_deducted, difference, locations[] {location_id, code, name, quantity, pending_pick, available, is_active}}` |
| `GET /stock/locations?location=R00-001` | Raf içeriği |
| `GET /stock/locations?unassigned=1&page=&size=` | Ürün × raf satırları (`unassigned=1`: yalnız `RAFSIZ` kovasındakiler) — "rafsız ürünleri filtreleyip raf tespit et". v1.23.11: her satırda `barcode, title, image, unassigned, physical, shelves` (ürünün şu anki rafları); `&suggest=1` ile `suggestion` (önerilen raf) |
| `GET /products?unassigned=1` · `?shelf=R00-001` · `?noshelf=1` | Katalog listesinde raf filtreleri; raflama açıkken her üründe `shelves{physical, unassigned, pending_pick, pending_deducted, locations[] {code, quantity}}` (detayda tam döküm + `difference`) |
| `GET /stock/summary` | `locations, shelved_quantity, unassigned_quantity, unassigned_products, pending_pick, negative_rows` |

### Hareketler
| Uç nokta | Açıklama |
|---|---|
| `POST /stock/movements` | Tek hareket ya da `{"items":[…], "by":"Ali"}` (≤500). Alanlar: `type`, `sku` (SKU ya da barkod) veya `product_id`, `quantity`, `location` / `from_location` / `to_location` (raf kodu ya da id; boş / `RAFSIZ` = rafsız), `note`, `ref`, `allow_negative`, `sellable` (varsayılan true). Yanıt — **tek hareket**: `movements[]`, `product{…döküm…}` (`201`; reddedilirse `422` + `message`). **Çok hareket** (`items`): `{"done":2,"failed":1,"results":[{"index":0,"ok":true,"error":null,"movements":[…],"product":{…}},…]}` — `index` gönderilen sıradır; en az biri işlendiyse `201`, hiçbiri işlenmediyse `422` (satır sebepleri yine `results`'ta). İstemci her satırın sonucunu `results[index]`'ten okumalıdır; aynı ürün birden çok satırda olabilir. Yetersiz raf stoğunda `422` (`"R00-001 rafında yalnız 2 adet var (istenen 5) — önce Sayım yapın"`). **Raf adedi hiçbir koşulda eksiye düşmez**: `allow_negative` artık eksiye düşürmez (v1.13.1), rafta gerçekten daha fazla varsa `type=adjust` (sayım) ile doğru adet girilir. |
| `GET /stock/movements?sku=&product_id=&location=&type=receipt,pick&order_id=&since=&after_id=&limit=100` | `items[] {id, sku, location, type, type_label, delta, quantity_after (rafta kalan), physical_after, sellable_after, group_id (transferin iki yüzü), ref_type (order\|return\|manual), ref_id, ref (sipariş no), order_item_id, note, by, source, created_at}`, `last_id` (artımlı okuma: `after_id`) |

| `type` | Etki | Ne zaman |
|---|---|---|
| `receipt` | + fiziksel (`location`), + satılabilir | Mal girişi (rafsız ya da doğrudan rafa) |
| `assign` | `RAFSIZ` → `to_location` | Rafsız ürünü rafa yerleştirme. Hedef raf `to_location` ile verilir; boşsa `location` kullanılır (hedef **rafsız olamaz**). `from_location` boşsa `RAFSIZ` varsayılır. |
| `transfer` | `from_location` → `to_location` | Raflar arası (iki hareket satırı, aynı `group_id`) |
| `consume` / `loss` | − fiziksel (`location` zorunlu: hangi raftan), − satılabilir | Sarf, kayıp / fire |
| `adjust` | rafın adedi = `quantity`; fark satılabilir stoğa da yansır | Sayım düzeltmesi |
| `return_in` | + fiziksel, + satılabilir | İade / geri gelen ürün girişi (iade akışında `POST /returns/{id}/restock` `location` ile otomatik) |
| `pick` / `unpick` | − / + fiziksel (satılabilir değişmez; sipariş zaten düşmüştü) | Sipariş toplama / geri alma (`/orders/{id}/pick`) |
| `cancel_restore` | + alındığı raf (raf silinmişse `RAFSIZ`) | Toplanmış kalem kargoya verilmeden iptal edildi |
| `init` | + `RAFSIZ` | Özellik açılışı / yeni ürün ilk stoğu |

Satılabilir stoğu değiştiren hareketler (`receipt`, `consume`, `loss`, `adjust`, `return_in`) ayrıca `GET /events`'e (`type: movement`, kaynak `mal_girisi | sarf | kayip | sayim | raf_iade`) düşer ve tüm pazaryerlerine yeni stok gönderilir. Her hareket `stock.moved` webhook olayı üretir.

### Ürün nerede? · raf önerisi (v1.23.11)
| Uç nokta | Açıklama |
|---|---|
| `GET /stock/where?code=` | Kod / barkod (ya da adın bir kısmı) ile ürün: `product{id, sku, barcode, title, image, sellable, physical, unassigned, pending_pick, difference, locations[] {location_id, code, name, zone, quantity, pending_pick, available, is_active}, last_movement{type, type_label, delta, location, created_at}}`, `suggestion`. Tam eşleşme yoksa `404` + `candidates[] {sku, barcode, title, image, sellable}` (en çok 8; tek aday varsa doğrudan o ürün döner). |
| `GET /stock/suggest?skus=A,B,C` | Toplu raf önerisi (en çok 200): `items{ "A": {code, location_id, reason, label, quantity} \| null }` |

`suggestion` = ürünün konacağı rafın önerisi (mal girişi, iade girişi, rafsızdan yerleştirme): `{code, location_id, reason, label, quantity, alternatives[]}`. Sıra (`reason`): `same_shelf` ürünün stoklu durduğu raf (birden çoksa en çok adetli) → `picked_shelf` (iade kalemi) bu siparişte toplandığı raf → `previous_shelf` en son durduğu raf → `variant` aynı modelin (`parent_sku`) diğer çeşitlerinin rafı → `category` aynı kategorinin en yoğun rafı → `none` (`code: null`). Yalnız aktif raflar önerilir; `label` Türkçe gerekçedir.

### Sipariş toplama
| Uç nokta | Açıklama |
|---|---|
| `GET /orders/{id}/pick` | `pick{state: none\|suggested\|partial\|picked, state_label, picked_at, picked_by, items[] {item_id, location, location_id, quantity, status: suggested\|picked}}` |
| `POST /orders/{id}/pick` | `{}` → önerilen dağılım aynen uygulanır. `{"items":[{"id":40 \| "line_id":"555","location":"R00-002","quantity":2}],"by":"Ali","force":false}` → belirtilen raflardan (önerilenden farklı olabilir; kısmi olabilir, kalan için öneri yenilenir). Raf stoğu yetersizse `422` (`force:true` ile raf eksiye düşer — sayım gerekir). Yanıt `picked, state, warnings[], items[] {item_id, sku, location, quantity, shelf_after}, pick{…}`; tüm kalemler toplanınca `orders.picked_at / picked_by` dolar, işlem geçmişine `pick` yazılır. |
| `POST /orders/{id}/pick/suggest` | Öneriyi raf stoklarına göre yeniden hesapla |
| `POST /orders/{id}/unpick` | Toplamayı geri al (ürünler aynı raflara geri, öneri yenilenir) |
| `POST /orders/pack-check` `{"code":"…","by":"Ali","pick":true}` | Paket kontrol; raflama açıksa `pick` (varsayılan: ayar) ile öneri "toplandı" sayılır → yanıtta `pick{ok, picked, state, items[]}` |
| `GET /orders/{id}/packing-slip` | İrsaliyede kalem görselinin altında raf adresleri (`✓` toplandı, `Rafsız` = raf belirsiz) |

### Raf ekranı (raf okut → ürün okut → adet gir → işlem)
Depocunun raf ekranı: raf etiketi okutulur, rafın içindekiler listelenir, ürün okutulur ve adet girilip dört işlemden biri seçilir. Panelde **Raflar**, WooCommerce eklentisinde **Pazaryeri Merkezi › Raflar** bu uçları kullanır.

| Uç nokta | Açıklama |
|---|---|
| `GET /stock/shelf?location=R00-001` | Raf + içindekiler: `location{id, code, name, …}`, `items[] {product_id, sku, barcode, title, image, quantity, pending_pick, available, sellable}`, `totals{products, quantity}`. `location=RAFSIZ` rafsız kovasını verir. |
| `GET /stock/shelf/product?location=R00-001&code=868…` | Okutulan ürün: `product{id, sku, barcode, title, image, on_shelf (bu rafta), unassigned (rafsız), physical, sellable, pending_pick (sipariş bekleyen), difference, locations[] (v1.23.11: ürünün bütün rafları), suggestion (önerilen raf)}` |
| `POST /stock/shelf` | `{"location":"R00-001","code":"868…","quantity":3,"op":"put\|receipt\|remove\|count\|move\|return_in","as":"loss","to_location":"R00-002","note":"","by":"Ali","allow_negative":false}`. `op`: **put** = rafsız stoğu bu rafa yerleştir (satılabilir değişmez) · **receipt** = mal girişi (+ fiziksel, + satılabilir) · **remove** = çıkar (sarf; `as:"loss"` ile kayıp/fire) · **count** = sayım (rafın **yeni** adedi) · **move** = `to_location` rafına taşı · **return_in** = iade girişi. Yanıt: `movements[]`, `product{…döküm…}`, `shelf{…GET /stock/shelf…}`, `undo[]` (geri alma için hareket id'leri), `message`. Raf stoğu yetmezse `422` + `insufficient` — **raf eksiye düşmez** (v1.13.1); rafta gerçekten daha fazla varsa `op:"count"` (sayım) ile doğru adet girilir. |
| `POST /stock/shelf/undo` | `{"movements":[56,57],"by":"Ali"}` → son işlemin tersini yazar (aynı rafa geri). Transferin tek yüzü verilse de iki satır birden döner; aynı hareket ikinci kez geri alınamaz (`ref_type='undo'`, `ref_id=<hareket>`). Yanıt `{undone, movements[], product, shelf}`. |

Raf stoğu değiştiğinde (yerleştirme, mal girişi, transfer, sayım, iade girişi) o ürünü bekleyen **açık siparişlerin rafsız kalmış raf önerileri** kendiliğinden yeniden hesaplanır: rafsızken satılan ürün sonradan raflanınca sipariş yeni rafa bağlanır. Gerçek bir rafa bağlanmış öneriye dokunulmaz.

### Depodan toplama (sipariş seçmeden — raf okut, ürün okut, adet gir)
Depocunun ekranı: sipariş seçilmez. Okutulan adet, o ürünü bekleyen **açık siparişlere en eskiden yeniye (FIFO)** dağıtılır. Panelde **Toplama** menüsü, WooCommerce eklentisinde **Pazaryeri Merkezi › Toplama** bu uçları kullanır.

| Uç nokta | Açıklama |
|---|---|
| `GET /stock/pick/queue?location=&code=&limit=100` | Toplanacaklar: `items[] {product_id, sku, barcode, title, image, need (bekleyen adet), orders (kaç sipariş), oldest_order_date, shelves[] {location_id, code, name, quantity}}`, `total_need`. `location=R00-001` verilirse yalnız o rafta stoğu olanlar; `code=` verilirse tek ürün (SKU / barkod). |
| `POST /stock/pick` | `{"location":"R00-001","code":"8680000000013","quantity":2,"by":"Ali","force":false}` → `{picked, left (girilen adetten dağıtılamayan), need_after, product{…}, location{…}, rows[] {id, order_id, order_number, marketplace, item_id, quantity, shelf_after}, orders[], warnings[]}`. Raf stoğu yetersizse `422` `insufficient` + `on_shelf` — **raf eksiye düşmez** (v1.13.1; `force` kaldırıldı): önce sayım yapılır. **Kısmi toplama serbesttir**: 2 adet bekleniyorken 1 girerseniz 1'i düşer, kalan ihtiyaç kuyrukta kalır. Tüm kalemleri toplanan sipariş `picked` olur. |
| `POST /stock/pick/undo` | `{"rows":[12,13],"by":"Ali"}` → son işlemin satırlarını geri alır: adet **aynı rafa** geri yazılır, öneri yenilenir, sipariş `picked` işareti kalkar. Yanıt `{undone, rows[], orders[]}`. |

`POST /stock/pick` her satır için `pick` hareketi (`GET /stock/movements?type=pick`), geri alma `unpick` hareketi yazar; ikisi de `stock.moved` webhook'u üretir.

---

## Web sitesi siparişleri (`marketplace=web`)

Kendi e-ticaret sitenizden (WooCommerce, OpenCart, Shopify eklentisi ya da özel yazılım) gelen siparişler pazaryeri gibi kaydedilir; paketleme, kargo, irsaliye, fatura, iade işlemlerinin tamamı panelden/API'den yapılır ve webhook ile sitenize geri bildirilir.

| Uç nokta | Açıklama |
|---|---|
| `POST /orders/web` | `{"external_id":"WC-1001","channel":"woocommerce","customer":"Ad Soyad","email":"","phone":"","address":"","city":"","district":"","bill_name":"","bill_company":"","bill_address":"","bill_city":"","bill_district":"","tax_number":"","tax_office":"","items":[{"sku":"KOLTUK-01","quantity":1,"price":4599.9},{"name":"Katalog dışı ürün","quantity":1,"price":99}],"total":4698.9,"currency":"TRY","payment_method":"kredi kartı","shipping_method":"Yurtiçi","order_date":"2026-09-12 10:00:00","note":"","deduct_stock":true,"cargo_account_id":null}` → `order` (standart sipariş nesnesi: `marketplace=web`, `order_number=external_id`, `channel`, `external_id`, `payment_method`, `shipping_method`). Aynı `external_id` tekrar gelirse `duplicate:true` ile mevcut sipariş döner (eklenti tekrar deneyebilir). `channel` verilmezse API anahtarının kanalı yazılır. |
| `GET /orders?marketplace=web` | Web siparişleri (`status`, `updated_since`, `q` filtreleri aynı) |
| `POST /orders/{id}/pack|ship|deliver|cancel|invoice`, `POST /orders/{id}/ship_create` | Yerel işlemler: `ship` `{"tracking_number":"","cargo_name":"Yurtiçi Kargo","tracking_url":""}`; sonuç sitenize `order.shipped` vb. webhook'la gider |

### Sipariş ek maliyetleri ve gelirleri (v1.23.0 — web sitesinin bildiği kâr kalemleri)
Siparişin kârına giren ama sipariş / pazaryeri verisinde olmayan tutarlar. **Kaynak** (`source`, ör. `site`) başına tutulur;
kaynak her gönderdiğinde o kaynağın satırları **tamamen yenilenir** (listede olmayan tür silinir). Tutarlar **KDV dahil**;
`vat_rate` > 0 ise içindeki KDV indirilecek KDV'ye (maliyette) ya da satış KDV'sine (gelirde) girer.

| `kind` | Kâr hesabında |
|---|---|
| `payment` | Ödeme altyapısı komisyonu (iyzico / PayTR / Shopier / kapıda ödemede kargonun tahsilat bedeli). **Kesinti kalemi** olur (`fee_lines[]` içinde `key:"payment_fee"`, `keep:true`, `note`); hakedişten (`payout`) önce düşülür. Bu satır varsa — **0 tutarlı olsa da** (havale: komisyonsuz; kapıda ödeme henüz tahsil edilmedi) — web kanalının "Sanal POS / ödeme komisyonu" oran kalemi o siparişe uygulanmaz. İade / iptal ters kaydı bu satıra dokunmaz (ödeme kuruluşu komisyonu iade etmez; kaynak isterse 0 gönderir) |
| `affiliate` | Satış ortaklığı komisyonu (nakit ödenen / ödenecek kısmı) — hakedişten sonra, vergi öncesi kârdan düşülür (`other_costs`, `other_lines[]`) |
| `marketing`, `other` … | Diğer sipariş maliyetleri — ortaklık gibi |
| `shipping_income` | Müşteriden alınan kargo ücreti — **ciroya eklenir** (`gross`, `sale_vat`; `income_lines[]`). Web siparişinde kalemler yalnız ürünleri taşır: sipariş toplamı = kalemler + bu gelirler |
| `fee_income` | Kapıda ödeme bedeli (müşteriden) — ciroya eklenir |

| Uç nokta | Açıklama |
|---|---|
| `GET /orders/{id}/costs` | `costs[] {id, kind, label, amount, vat_rate, vat, note, meta, source, updated_at}`, `kinds` |
| `PUT /orders/{id}/costs` | `{"source":"site","external_id":"YK2609281234","costs":[{"kind":"payment","label":"Ödeme komisyonu","amount":43.82,"vat_rate":0,"note":"iyzico · iyzico'nun bildirdiği tutar"},{"kind":"affiliate","amount":85,"note":"ORTAK1"},{"kind":"shipping_income","amount":50,"vat_rate":20}]}` → `costs`, `changed`, `profit`. `costs:[]` → kaynağın satırları silinir. Aynı tür iki kez → toplanır. `external_id` verilirse siparişin numarasıyla (`external_id` / `order_number` / `remote_id`, büyük-küçük harf duyarsız) eşleşmezse **409 `mismatch`** — hiçbir şey yazılmaz |
| `POST /orders/costs` | Toplu (en çok 500): `{"source":"site","orders":[{"order_id":12,"external_id":"YK2609281234","costs":[…]},{"external_id":"YK2609285678","costs":[…]}]}` → `updated, changed, missing[], errors[] {index, order_id, code, error}`. Yalnız `external_id` verilirse sipariş numarasıyla bulunur (önce web siparişi). Kimlik + numara birlikte verilir ve eşleşmezse o satır **yazılmaz**, `errors[]` içinde `code:"mismatch"` — site eski bir kurulumun / başka hesabın sipariş kimliğini saklıyorsa (Pazaryeri Merkezi yeniden kuruldu) başka bir siparişin maliyetleri ezilmez |

Dönem raporunda (`GET /finance/summary`): `other_costs`, `other_costs_vat`, `other_costs_by_kind[] {kind, label, amount, vat, count}`,
`income_by_kind[] {kind, label, amount, count}` (iade / iptal payı düşülmüş), `fee_lines[]` içinde "Ödeme komisyonu (web sitesi)".
Sipariş kârlılığında (`/finance/orders`) satırda `payment_fee`, `other_costs`; ürün kârlılığında ek maliyet kalemlere ciro payıyla dağıtılır.

### Siparişi platformdan silme (iptal DEĞİL)
`DELETE /orders/{id}?purge=1` — sipariş **hiç gelmemiş gibi** silinir; **pazaryerine hiçbir bildirim gönderilmez** (iptal isteği yok). Yeniden test için ve yanlış düşen kayıtları temizlemek için.

- düşülen **satılabilir stok geri yüklenir** → tüm pazaryerlerine ve sitenize yeni stok gider (`stock.changed`, `GET /events`)
- toplanmışsa **fiziksel adet alındığı rafa** geri yazılır
- siparişin **stok hareketleri** (`GET /stock/movements?order_id=`), raf önerileri, kalemleri, işlem geçmişi ve faturaları silinir
- `&ignore=1` → sipariş "yok say" listesine eklenir, bir daha çekilmez. Verilmezse sonraki senkronda pazaryerinden **tekrar gelir**: silme sırasında bağlantının **senkron imleci** (`orders_cursor`) siparişin tarihine geri sarılır ve `orders_refresh_at` temizlenir, böylece sonraki tur tam pencereyi tarar. Sipariş pazaryerinin geriye dönük penceresinden eskiyse yanıttaki `reimport` alanı bunu söyler (canlı çekim getiremez, "Geçmişi çek" gerekir).
- Yanıt: `{deleted, stock_restored, shelf_restored, movements_deleted, ignored, reimport}`. Kargoya verilmiş **kendi** gönderiniz varsa `422` (önce gönderiyi iptal edin). Webhook: `order.deleted`.
- `purge` verilmezse manuel gönderi / web siparişi için eski davranış (kargo iptali + stok iadesi) geçerlidir.

## Giden webhook'lar (sitenize POST)

| Uç nokta | Açıklama |
|---|---|
| `GET /webhooks` | Uç noktalar (`secret` dahil) |
| `POST /webhooks` | `{"url":"https://siteniz.com/bayir-hook","events":["order.created","order.updated","order.shipped","order.delivered","order.cancelled","return.created","return.updated","question.created","invoice.created","stock.low","stock.changed","stock.moved","order.deleted","order.late_cancel"],"label":"Sitem","secret":"isteğe bağlı"}` (events boş = tüm olaylar). `stock.moved`: raflama hareketi (mal girişi, yerleştirme, transfer, toplama, sarf, kayıp, sayım, iade girişi) — `data{movement{…}, product{id, sku, sellable, physical}}`. `order.deleted`: sipariş platformdan silindi (v1.13.1). **`order.late_cancel`** (v1.13.2): sipariş iptal / iade edildi ama **kargo çoktan yola çıkmıştı**, gönderi iptal edilemedi — `data{order{…}, shipment{cargo_key, tracking_number, status}}`; paketi durdurmak için kargo firmasıyla görüşülmelidir |
| `PUT /webhooks/{id}` | `{"url","events","label","is_active","secret"}` |
| `DELETE /webhooks/{id}` | Sil |
| `POST /webhooks/{id}/test` | `ping` olayı hemen gönderilir, sonuç döner |
| `GET /webhooks/{id}/deliveries?limit=50` | Teslimat geçmişi (`status: pending|sent|failed|skipped`, `attempts`, `last_code`, `last_error`, `next_at`) |
| `POST /webhooks/deliveries/{id}/retry` | Teslimatı yeniden kuyruğa al |

Teslimat: `POST {url}` gövde `{"event":"order.created","created_at":"…","account_id":1,"data":{"order_id":12,"order_number":"WC-1001","marketplace":"web","order":{…tam sipariş, items dahil…}}}`; başlıklar `Content-Type: application/json`, `X-Bayir-Event`, `X-Bayir-Delivery` (teslimat id), `X-Bayir-Signature: sha256=HMAC_SHA256(gövde, secret)`, `User-Agent: {Marka}-Webhook/2.0` (marka adı Yönetim › Marka ve kimlik ekranından). 2xx dışı yanıt ya da zaman aşımında 60 sn, 5 dk, 15 dk, 1 sa, 2 sa, 6 sa, 12 sa, 24 sa sonra yeniden denenir (8 deneme); 50 ardışık hatada uç nokta pasife alınır. Aynı teslimat id tekrar gelebilir → sitenizde idempotent işleyin.

PHP doğrulama örneği:
```php
$body = file_get_contents('php://input');
$sig  = $_SERVER['HTTP_X_BAYIR_SIGNATURE'] ?? '';
if (!hash_equals('sha256=' . hash_hmac('sha256', $body, $secret), $sig)) { http_response_code(401); exit; }
$evt = json_decode($body, true); // $evt['event'], $evt['data']['order']
http_response_code(200);
```

## Hesap kullanıcıları ve roller

| Uç nokta | Açıklama |
|---|---|
| `GET /account/members` | `items[] {id,email,name,role,role_label,is_active,last_login_at}`, `roles` (rol → etiket), `descriptions` |
| `POST /account/members` | `{"email":"","password":"(≥8)","name":"","role":"admin|muhasebe|depo|destek|viewer"}` → `id` |
| `PUT /account/members/{id}` | `{"role","name","is_active","password"}` |
| `DELETE /account/members/{id}` | Sil |

Roller panel içindir; API anahtarları her zaman hesap sahibi yetkisiyle çalışır. Ek anahtarlar API sayfasından (kanal etiketiyle) üretilir.

## Kârlılık: kesintiler, ürün maliyeti, desi tarifesi

| Uç nokta | Açıklama |
|---|---|
| `GET /orders/{id}` → `profit`, `GET /orders/{id}/profit` | Kâr dökümü, **tüm tutarlar KDV dahil**: `gross, sale_vat, commission (KDV hariç), commission_vat (komisyon tutarının İÇİNDEN ayrılan KDV — üstüne eklenmez), commission_incl (= pazaryerinin bildirdiği tutar) (+commission_source, commission_rate), fee_lines[] {key,label,amount,vat,total,refund,keep,note}, fees, fees_vat, fees_incl, stopaj, payout (hesaba geçecek), cargo, cargo_vat, cargo_source (table|manual|shipment|default), cogs, cogs_vat, cost_missing_qty, pretax_profit (vergi öncesi), deductible_vat, vat_payable, tax_base, tax_rate, profit_tax, tax_due (stopaj mahsuplu), profit (vergi sonrası net), margin, reversals[] (revenue_reversed, commission_refunded, fees_refunded, fees_kept, cargo_refunded, return_cargo, cogs_restocked, cogs_lost, pending_qty, pretax_impact, profit_impact), reversal_share, reversal_loss, final_figures, final_payout, final_pretax_profit, final_profit, other_costs, other_costs_vat, other_lines[] {kind,label,amount,vat,vat_rate,note,source}, income_lines[] …`. `?profit=0` ile kapatılır. |
| `GET|PUT /settings/tax` | `{"rate":25,"enabled":true,"label":"Kurumlar vergisi"}` — kâr vergisi oranı (şahıs: gelir vergisi dilimi). Vergi = (vergi öncesi kâr − ödenecek KDV + stopaj) × oran; stopaj mahsup edilir. |
| `PUT /orders/{id}` | `{"parcel_count":2,"desi":12.5,"slip_detailed":1,"slip_labels":null,"cargo_cost":89.9,"customer":"","phone":"","email":"","address":"","city":"","district":"","note":""}` — `slip_detailed/slip_labels`: irsaliye dağılımı (null = otomatik 1 + kalanı); koli/desi girilince `box_source=user` olur, pazaryerine bildirilir (yanıtta `box_info`); çıktıda `mp_parcel_count`, `mp_desi` (pazaryerinin bildirdiği), `box_source`, `slip_split` — koli/desi girilince kargo maliyeti tarifeden yazılır (yanıtta `cargo_cost {cost,source,detail}`); `cargo_cost` elle girilirse tarife ezilmez, `null` gönderilince tarifeye dönülür; telefon `""` ile silinir. Gönderilen alıcı alanları (`customer, phone, email, address, city, district`) kilitlenir: sonraki pazaryeri çekimleri bunları ezmez (`user_fields`). **Fatura (alıcı) bilgisi** (1.29.0): `bill_name, bill_company, tax_number` (10 hane VKN / 11 hane TCKN), `tax_office, bill_address, bill_city, bill_district, bill_phone, bill_email` — il + ilçe RESMİ adlara çevrilir (`"Amasya Merkez"` → `Merkez`; Logo İşbaşı yalnız resmi adı kabul eder); resmi ilçe değilse 422 (`options` = ilin resmi ilçeleri; `force_address:true` olduğu gibi kaydeder); gönderilen alanlar kilitlenir; yanıtta `order.invoice_address` (faturaya yazılacak geçerli adres, `valid`, `suggest`, `districts`), `order.invoice` (sipariş detayında faturanın durumu: `{id,status,number,error,error_hint}` ya da `null`; `error_hint:"bill_address"` = hata il / ilçe adından) ve `notes` |
| `GET /address/provinces`, `GET /address/districts?city=Amasya&district=Amasya Merkez` | Resmi il listesi; ilin resmi ilçeleri (+ verilen ilçenin resmi karşılığı `resolved`) |
| `DELETE /orders/{id}` | Yalnız **manuel gönderi** (`marketplace=manual`): açık kargo gönderisi web servisten iptal edilir, düşülen stok geri verilir, kayıt silinir; pazaryerine bildirim yapılmaz. Kargoya verilmiş gönderi silinemez (422). Yanıt: `{deleted, shipment_cancelled, stock_restored, parent_order_id}` |
| `POST /orders/pack-check` | `{"code":"YRK123","by":"Ahmet"}` — paket kontrol: paketteki barkod (kargo anahtarı / sipariş no / paket no / takip no) → sipariş `pack_checked_at` işaretlenir; irsaliyesi hiç basılmamışsa `warnings[]` (etiketsiz paket). `POST /orders/slip-printed {ids[]}` irsaliye basıldı işareti (panelden yazdır/PDF otomatik). Siparişte `slip_printed_at`, `pack_checked_at`. |
| `GET|PUT /connections/{id}/fees` | Kesinti kalemleri: `{"fees":[{"key":"commission_vat","label":"Komisyon KDV’si","type":"pct_commission","value":20,"vat":0,"refund":true,"enabled":true},{"key":"service_fee","label":"Platform hizmet bedeli","type":"fixed_order","value":10.99,"vat":20,"refund":false},{"key":"stopaj","type":"pct_sale_exvat","value":1}]}` ya da `{"reset":true}` (pazaryeri varsayılanları). Türler: `pct_sale` (KDV dahil satış %), `pct_sale_exvat`, `pct_commission`, `fixed_order` (sipariş başına ₺ KDV hariç), `fixed_item`. Yanıtta `defaults` (2026 güncel değerler) ve `types`. |
| `GET|PUT /settings/web-fees` | Web sitesi / manuel sipariş kesintileri (sanal POS vb.), aynı biçim |
| `GET|PUT /settings/cargo-rates` | Desi tarifesi: `{"rows":[{"min":1,"max":10,"price":350},{"min":11,"max":30,"price":550},{"min":31,"max":0,"price":900}],"mode":"per_parcel|total","extra_parcel":0,"default_desi":0,"return_multiplier":1,"vat_included":true,"vat_rate":20}` — `max` 0 = sınırsız; `per_parcel`: toplam desi kolilere bölünüp her koli tarifelenir; `total`: toplam desiye tek ücret + ek koli bedeli. Yanıtta `examples`. |
| `POST /products` / `PUT /products/{sku}` `{"cost":2800}` | Ürün maliyeti (**KDV dahil**); `""`/`null` kaldırır. `GET /products` çıktısında `cost`; sipariş kalemlerinde `cost`, `vat_rate` (sipariş anındaki anlık kayıt) |
| `PUT /returns/{id}` | `{"return_cargo_cost":120,"note":""}` — dönüş kargo bedeli (boş = tarife × çarpan) |
| `PUT /orders/{id}/items/{item_id}` | `{"cost":600,"mode":"fill|order|product","vat_rate":20}` — kalem maliyeti (KDV dahil). `fill` (varsayılan): ürün kartında maliyet boşsa oraya da yazar; `product`: her zaman günceller; `order`: yalnız bu sipariş; `cost:null` kaldırır. Yanıt: `item`, `product_updated`, `profit` |
| `POST /orders/manual` `{"source_order_id":12,"cargo_cost":120,"items":[{"name":"Menteşe","quantity":2,"cost":45}],…}` | Ek gönderi: kargo (elle ya da desi tarifesi) + malzeme (kalemlerin birim maliyeti; yoksa katalog; `material_cost` elle toplam) ana siparişin kârından düşülür; `GET /orders/{id}/profit` → `extras[] {order_id, order_number, reason, date, status, cargo, cargo_source, cargo_known, material, charge_material, material_override, cost_missing, items, lines[], total}`, `extra_cargo`, `extra_cogs`; ek gönderide maliyeti girilmemiş kalem ana siparişin `cost_missing_qty` değerine de eklenir |
| `GET /finance/summary?since&until` | KDV dahil dönem raporu: `revenue, sale_vat, net_sales, commission, commission_vat, commission_incl, fees, fees_vat, fees_incl, stopaj, payout, cargo (gidiş + iade dönüşü), cargo_vat, return_cargo, cogs, cogs_vat, cogs_lost, cancelled, returns_loss (iade/iptal kaybı), returns_impact, pretax_profit, deductible_vat, vat_payable, tax_base, profit_tax, tax_due, profit (vergi sonrası, giderler dahil), expenses, expenses_vat, operating_profit / pretax_net (= pretax_profit − expenses), tax_total, net (= operating_profit − vat_payable − tax_due; v1.23.0 öncesinde giderler vergiden sonra düşülüyordu), extras_count, extras_total, extra_cargo, extra_cogs, cargo_unknown, cost_missing, pending_restock, fee_lines[], expenses_by_category[] {category, label, amount, vat, count}, tax, checks[]`; `by_marketplace[]` aynı kırılım; tam iade/iptal edilen siparişler `orders` ve `revenue`'ya girmez |

Kâr formülü (KDV dahil): `payout = gross − commission_incl − fees_incl − stopaj` → `pretax_profit = payout − other − cargo − cogs` (v1.23.0: `other` = sipariş ek maliyetleri, ör. satış ortaklığı; `gross` web siparişinde müşteriden alınan kargo ücreti ve kapıda ödeme bedelini de içerir) → (dönem raporunda) `operating_profit = pretax_profit − opex` → `vat_payable = sale_vat − (commission_vat + fees_vat + other_vat + cargo_vat + cogs_vat + opex_vat)` → `profit_tax = max(0, operating_profit − vat_payable + stopaj) × oran` → `profit = operating_profit − vat_payable − (profit_tax − stopaj)`. Siparişte `opex` 0'dır. İade: ciro/KDV geri, komisyon (+KDV) + `refund` işaretli kalemler + stopaj geri, gidiş + dönüş kargosu maliyet, stoğa giren ürün gider değil, hasarlı ürün maliyeti gider; iptalde her şey geri (sonuç 0).

## Hız sınırı

Her API anahtarı için dakikalık limit (varsayılan 300, API sayfasından anahtar başına ayarlanır); yanıtta `X-RateLimit-Limit`, aşımda `429` + `Retry-After`. Pazaryeri tarafına giden istekler bağlantı başına pencerelenir (ör. Trendyol 40 istek / 10 sn) — limit dolunca istek reddedilmez, kısa süre bekletilir.

## Kural bazlı fiyatlama

| Uç nokta | Açıklama |
|---|---|
| `GET /connections/{id}/price-rules` | `base_rule {price_mode, price_value}`, `rules[]` (+`summary`) |
| `PUT /connections/{id}/price-rules` | `{"rules":[{"name":"Koltuklar","match":"category","value":"Koltuk","percent":18,"fixed":0,"round":"90","list_percent":0,"disabled":false},{"match":"price_between","min":0,"max":500,"percent":25,"round":"99"},{"match":"all","percent":10}]}` — sıra = öncelik; **ilk eşleşen** kural uygulanır, hiçbiri eşleşmezse genel kural (`price_mode/price_value`). `match`: `all`, `category` (içeren), `brand` (tam), `sku_prefix` (SKU/barkod başlangıcı), `sku_contains` (SKU/ürün adı içeren), `price_between` (`min`–`max`, max 0 = sınırsız). `round`: `none`, `int` (yukarı tam sayı), `90`, `99`. `list_percent` > 0 ise ürünün liste fiyatı yoksa satış × (1+%) liste fiyatı olarak gönderilir. Pazaryerine özel fiyat (`PUT /products/{sku}/prices`) girildiyse kurallar atlanır. Kaydedince eşleşmeler yeniden gönderime alınır. |

## Gelen webhook'lar (pazaryeri → biz)

| Uç nokta | Açıklama |
|---|---|
| `GET /connections/{id}/inbound-webhook` | `enabled`, `url`, `secret`, `basic_user` (HB), `last_received_at`, `count`, `recent[]` |
| `POST /connections/{id}/inbound-webhook` | `{"reset":false,"register":true,"lax":false,"unregister":false}` — adres/anahtar üretir; `register` Trendyol'a kaydeder (`registered {ok,error,id}`); `lax` başlık doğrulamasını kapatır (yalnız URL token'ı); `unregister` Trendyol'daki eski kayıtları siler |
| `DELETE /connections/{id}/inbound-webhook` | Kapatır; `?unregister=1` (ya da gövdede) Trendyol'daki kayıtları da siler (`unregistered {items[], deleted}`) |

Adres `{base}/hook/trendyol/{token}` ya da `{base}/hook/hepsiburada/{token}`; Hepsiburada adresi Basic auth (kullanıcı = token, şifre = secret) ile HB destek ekibine "base URL" olarak verilir.

## Bildirimler

| Uç nokta | Açıklama |
|---|---|
| `GET /settings/notifications` · `PUT /settings/notifications` | Bildirim tercihleri: `types{}` ve `preferences{order.new: true, …}`; PUT gövdesi `{"order.new": false, …}` |
| `GET /settings/stock-webhook` · `PUT /settings/stock-webhook` | Eski tip stok olayı webhook'u (tek adres): `{"url": "https://…", "regenerate_secret": true}` → `url, secret` (yeni entegrasyonlar için `/webhooks` kullanın) |
| `GET /notifications?limit=50` | `unread`, `items[] {id,type,title,body,url,read,data,created_at}` — türler: order.new, order.status, return.new, return.restock (v1.23.11: stok kararı hatırlatması, günde en fazla bir, 09–21; `url` İadeler › Stok kararı bekleyen), question.new, cargo.tracking, cargo.delivered, invoice.error |
| `POST /notifications/read` `{"id":12}` (id yoksa tümü) | Okundu işaretle |

Tarayıcı push bildirimleri panelden (zil menüsü) açılır; olaylar ayrıca `GET /orders?updated_since=` ve webhook ile izlenebilir.

---

## Müşteri soruları

| Uç nokta | Açıklama |
|---|---|
| `GET /questions?status=waiting|answered|closed|all&unread=1&from=operator|customer&page=&size=` | `total`, `items[]` — `id, marketplace, remote_id, product_name, product_code, question, customer, answer, status, answer_state, answer_error, answer_to, asked_at, expires_at, answered_at, unread, read_at, reply_needed, last_from, last_in_at` + görüşme alanları (aşağıda). v1.26.0: `unread=1` okunmamış gelen mesajlar, `from=operator` son mesajı Koçtaş operatörü yazmış görüşmeler; bu süzgeçlerle `status` varsayılanı `all` olur. Sıralama son gelen mesaja göre |
| `POST /questions/{id}/answer` `{"text":"…","to":"customer|operator|both"}` | Cevap sıraya alınır, işçi pazaryerine iletir (`answer_state`: queued → sent ya da failed; `answer_error`: pazaryerinin hata metni, Türkçe açıklamasıyla). `to` yalnızca Koçtaş (Mirakl) için; **boşsa son mesajı yazan** taraf seçilir (v1.23.10). Cevaplamak soruyu okundu yapar. **409** `already_answered` / `question_closed`: tek cevaplı pazaryerinde (Trendyol, Hepsiburada, N11, İdefix, Çiçeksepeti) soru zaten cevaplanmış / kapanmışsa ikinci cevap gönderilmez (Koçtaş görüşmelerinde koruma yok) |
| `POST /questions/{id}/read` | v1.26.0: soru / görüşme okundu (`{"ok":true,"read_at":"…"}`) — okunmamış sayacından (`GET /dashboard` → `questions_unread`) düşer. `id` bizim numaramız ya da `?marketplace=` ile pazaryeri kimliği |

**Okunmamış mesajlar (v1.26.0):** her yeni GELEN mesaj (müşteri ya da Koçtaş operatörü — cevap gereksin gerekmesin, operatörün "bilgi" notu dahil)
`last_in_at` yazar, `read_at` boşalır (`unread: true`), `question.new` bildirimi + `question.created` webhook'u üretir. `reply_needed` (yalnız Koçtaş): pazaryeri
mağaza cevabı bekliyor mu (`false` = bilgi notu; `status` o zaman `answered` kalır, soru "Cevap bekleyen"de değil "Okunmamış" / "Koçtaş operatörü"nde görünür).
`last_from`: son mesajı kim yazdı (`customer` | `operator` | `shop`). Diğer pazaryerlerinde yeni soru `last_in_at = asked_at` ile okunmamış başlar.

**Pazaryerinde zaten cevaplanmış (v1.26.0):** `answer_state: "remote"` — soru pazaryerinde (panel / uygulama / başka araç) cevaplanmıştı; pazaryerinin
cevabı `answer`a çekildi, `answered_at` pazaryerinin tarihi, `answer_error` boş. Hata değildir; gri not olarak gösterin. Bizim gönderemediğimiz metin
Kayıtlar'da (`answers` kanalı) durur. Cevap gönderimi "zaten cevaplandı" hatası alınca (Trendyol `business.rule.question.has.already.answered`) ya da
işçinin uzlaştırma adımı (takılı / 13 günden eski bekleyen sorular, 6 saatte bir, tek soru sorgusu: Trendyol `GET …/questions/{id}`, İdefix
`GET …/question/{id}`, N11 `GetProductQuestionDetail`) pazaryerinin durumunu alır. Süresi dolan / reddedilen sorular `status: closed` olur.

**Çiçeksepeti sipariş sohbetleri (v1.26.0):** Çiçeksepeti'nin "satıcıya sor" sipariş soruları (`GET /api/v1/chats`) da soru listesine gelir:
`remote_id` = `oc:{chatId}`, `product_name` = "Sipariş #… · ürün adı", `customer` = "Müşteri" ya da "Çiçeksepeti müşteri temsilcisi"
(chatType 2). Görüşme alanları Koçtaş'taki gibi doludur (`from`, `order_number`/`order_id`, `messages[]`, `reply_to`, `recipients`);
cevap `POST /questions/{id}/answer {"text":…}` ile sohbete mesaj olarak gider (dosya eki henüz desteklenmez). Sohbet kapanmışsa
(`isActive=false`) kayıt `closed` olur. **Çiçeksepeti ürün sorusunda cevap türü:** `to=customer` → cevap yalnız soran müşteriye gider,
sitede yayınlanmaz (Çiçeksepeti branchActionId 2, alt tür "Diğer"); `to` boş → soru ve cevap sitede yayınlanır (1). "Uygunsuz içerik" (3) ve
"başka satıcıya ait soru" (4) işlemleri Çiçeksepeti satıcı panelinden yapılır.

**Koçtaş (Mirakl) görüşmeleri (v1.23.10):** Koçtaş'ta "soru" bir görüşmedir — müşteri ve Koçtaş operatörü aynı konuya defalarca yazabilir.
Konu başına tek kayıt vardır: `question` son gelen (müşteri / operatör) mesaj, `answer` mağazanın son mesajı. Görüşmeye yeni mesaj gelince
kayıt yeniden `status: waiting` olur (önceki cevabımız gönderilmişse `answer_state` sıfırlanır) ve **`question.new` bildirimi /
`question.created` webhook'u** yeniden gelir (`followup: true`). Ek alanlar (diğer pazaryerlerinde `null` / boş):

| Alan | Açıklama |
|---|---|
| `from` | Son gelen mesajı kim yazdı: `operator` (Koçtaş ekibi) \| `customer` |
| `customer_visible` | Son gelen mesaj müşteriye görünüyor mu — operatörün yalnız mağazaya yazdığı not için `false` ("Müşteriye görünmez") |
| `order_number`, `order_id` | Sipariş görüşmesiyse pazaryeri sipariş numarası ve bu hesaptaki sipariş kaydı (varsa) |
| `reply_to` | Varsayılan cevap alıcısı: son mesajı yazan (`operator` \| `customer`) |
| `recipients` | Cevap verilebilecek taraflar (konunun katılımcıları). Müşterinin katılımcı olmadığı operatör konusunda yalnız `["operator"]`; müşteriye gönderim Koçtaş'ta reddedilir |
| `messages[]` | Konuşma, eskiden yeniye (son 30): `{from: customer\|operator\|shop, name, to[], customer_visible, body, date, attachments}`. Gönderdiğimiz cevap, sorular yeniden çekilmeden konuşmaya eklenir |
| `answer_to` | Son cevabımızın gerçekte gittiği taraf: `customer` \| `operator` \| `both` |

Bildirim / webhook verisi (`question.new` → `question.created`): `marketplace, question, product, question_id` + Koçtaş'ta `from`, `order_number`,
`customer_visible`, yeni mesajda `followup: true`. Bildirim başlığı: "Koçtaş operatör mesajı · #sipariş", "Koçtaş müşteriden yeni mesaj",
"Koçtaş yeni müşteri sorusu".

Soru desteği: Trendyol, Hepsiburada, N11, Çiçeksepeti, İdefix, Koçtaş (Mirakl mesajları). Pazarama ve PttAVM API'lerinde soru uç noktası yoktur.

---

## Stok olayları (çift yönlü senkron)

Pazaryerinde sipariş oluşunca stok düşülür, diğer pazaryerlerine yeni stok gönderilir ve üyeye olay üretilir.

| Uç nokta | Açıklama |
|---|---|
| `GET /events?after_id=0&unacked=1&limit=200` | `events[] {id,type,source,sku,delta,stock_after,marketplace,order_number,acked,created_at}`, `last_id`, `has_more`. `type`: `order` (pazaryeri / web siparişi düşümü) · `cancel` (iptal → stok geri) · `return` (iade girişi) · `movement` (raflama: `mal_girisi`, `sarf`, `kayip`, `sayim`, `raf_iade`) · `reset` (`red_sifirla`: tedarik edememe iptalinde stok sıfırlandı) · `adjust` (`site`: sitenizin `stock_delta` ile gönderdiği fark; onaylı gelir, `order_number` = gönderdiğiniz `ref`). `marketplace` alanı ham kaynağı verir (`trendyol`, `trendyol_iptal`, `sarf` …) |
| `POST /events/ack` `{"up_to_id":123}` veya `{"ids":[…]}` | Sitenizde stoğu işledikten sonra onay |

Webhook alternatifi: Ayarlar › Webhook adresi. Olaylar `X-Bayir-Signature: sha256=HMAC-SHA256(gövde, gizli anahtar)` ile POST edilir;
2xx yanıt onay sayılır, başarısızlar artan aralıklarla yeniden denenir.

Satılabilir stoğu değiştiren her şey buradan akar: sipariş düşümleri, iptal / iade geri girişleri, raflama hareketleri (mal girişi, sarf, kayıp, sayım) ve "red → stok sıfırlama". Fiziksel (raf) hareketlerinin tamamı ise `GET /stock/movements` ve `stock.moved` webhook olayında.

---

## WordPress eklentisi güncellemesi

WooCommerce eklentisi kendi güncellemesini bu iki uçtan alır; WordPress'in Eklentiler sayfasında
"Güncelleme var" uyarısı çıkar ve tek tıkla kurulur. Paketi WordPress.org değil, bu sunucu dağıtır.

### `GET /plugin/wordpress` — yayındaki sürüm
```json
{"success":true,"plugin":{
  "slug":"bayir-pazaryeri","name":"Pazaryeri Merkezi","available":true,
  "version":"1.9.1","requires":"6.0","tested":"6.7","requires_php":"7.4","requires_wc":"7.0",
  "download_url":"https://app.bayircommunity.com/api/v1/plugin/wordpress/download",
  "changelog":"* ...","released_at":"2026-09-19 23:03:21",
  "sha256":"60db37d3…","size":375055,
  "homepage":"https://app.bayircommunity.com/docs","author":"Platformunuzun adı"}}
```
Yayında sürüm yoksa `available:false` ve `version:"0.0.0"` döner (kurulu her sürüm bundan büyüktür,
yani güncelleme uyarısı çıkmaz).

### `GET /plugin/wordpress/download` — paketi indir
`Content-Type: application/zip`, `Content-Disposition: attachment; filename="bayir-pazaryeri-1.9.1.zip"`,
`X-Checksum-SHA256` ve `X-Plugin-Version` başlıklarıyla döner. **API anahtarı zorunludur**; anahtarsız
istek `401` alır. Sunucu, dosyayı göndermeden önce SHA-256 özetini yeniden hesaplar; tutmazsa `409`
(`corrupt_package`) döner. Her başarılı indirme sayaca işlenir.

Her iki uç da **salt okuma** olduğu için aboneliği bitmiş hesaplarda da çalışır: güvenlik yaması
kimseye kapatılmaz (yazma işlemleri zaten `402` ile kilitlidir).

Sürümleri panelden yönetirsiniz: **Yönetim › WordPress eklentisi** (`?p=wpplugin`).

---

## Eski eklenti uyumluluğu
`/{pazaryeri}/status`, `/{pazaryeri}/sync/stock`, `/{pazaryeri}/questions`, `/{pazaryeri}/orders`, `/{pazaryeri}/returns`
(`trendyol, hepsiburada, n11, pazarama, ciceksepeti, idefix, koctas, pttavm`).

---

## Entegrasyon örnekleri

### Kendi e-ticaret yazılımı (PHP)
```php
$api = fn(string $m, string $p, array $b = []) => json_decode(file_get_contents('https://app.bayircommunity.com/api/v1' . $p, false, stream_context_create(['http' => [
    'method' => $m, 'header' => "X-Api-Key: bc_live_XXX\r\nContent-Type: application/json\r\n", 'content' => $b ? json_encode($b) : null, 'ignore_errors' => true]])), true);

$api('POST', '/stock', ['items' => [['sku' => 'KOLTUK-01', 'stock' => 12, 'price' => 4599.90]]]);
$new = $api('GET', '/orders?status=new&include=items');
$api('POST', '/orders/15/shipment', ['account_id' => 1, 'parcel_count' => 2]);   // kendi Yurtiçi hesabınızla gönderi
file_put_contents('irsaliye.pdf', file_get_contents('https://app.bayircommunity.com/api/v1/orders/15/packing-slip.pdf', false, stream_context_create(['http' => ['header' => "X-Api-Key: bc_live_XXX\r\n"]])));
$api('POST', '/orders/16/ship', ['cargo_company' => 'Yurtiçi', 'tracking_number' => '1234567890']); // pazaryeri anlaşmalı kargo
$ev = $api('GET', '/events?unacked=1');           // pazaryeri satışlarını kendi stoğunuza uygulayın
$api('POST', '/events/ack', ['up_to_id' => $ev['last_id']]);
```

### WooCommerce eklentisi (hazır): Pazaryeri Merkezi
`bayir-pazaryeri.zip` — WordPress › Eklentiler › Yeni ekle › Eklenti yükle. Kurulunca **Pazaryeri Merkezi › Ayarlar**'a API anahtarı girilir; eklenti bağlantıyı test eder, webhook'u kaydeder (`POST /webhooks` tüm olaylar + `PUT /settings/stock-webhook`) ve ilk senkronu başlatır.
**v1.5.1**: Sipariş kutusunda **"Siparişi platformdan sil"** (iptal değil — pazaryerine bildirilmez; stok ve raf adedi geri yüklenir, WooCommerce siparişi çöpe taşınır, isteğe bağlı "bir daha çekilmesin"). Raf ekranlarında **"yine de düş" kaldırıldı** (raf eksiye düşmez; yetersizse Sayım önerilir) ve eksiye düşmüş kayıtlar için **Sıfırla** düğmesi. Sipariş listesinde Trendyol **"Onay bekliyor — işlem yapmayın"** uyarısı.

**v1.5.0**: **Raflar** sayfası Toplama ile aynı mantıkta yeniden yazıldı (telefon için): raf okut → ürün okut → adet gir → **Rafa koy / Mal girişi / Çıkar / Sayım**; rafın içindekiler görselli listede, satırdan ürün seçme ve **başka rafa taşı**, her işlemde **Geri al**. Raf seçicilerinde ve ürün alanında kamera. **Toplu stok işlemleri** aynı telefon düzenine geçti (büyük alanlar, kamera, raf etiketini okutup raf seçme, satırlar kart görünümü). Düzeltmeler: "Rafa yerleştir" seçilince hedef raf alanının gizlenmesi (hareket hiç işlenmiyordu).

**v1.4.0**: **Toplama** ve **Paket kontrol** ayrı menüler oldu (telefon için tek sütun ekran). Toplama sipariş seçmez: raf okut → ürün okut → adet gir (`GET /stock/pick/queue`, `POST /stock/pick`, `POST /stock/pick/undo`); kısmi toplama, satır satır geri alma, raf seçili kalır. Paket kontrol artık **toplama yapmaz** (`pack-check` `pick:false`; eski davranış Ayarlar › Özellikler'den açılabilir). Sipariş ekranındaki toplama kutusu kaldırıldı, yerine durum rozeti + Toplama bağlantısı kondu. Ürün detayında **🏷 Barkod etiketi**: adet + düzen seçip önizleme / yazdırma / PDF (`GET /products/{sku}/labels(.pdf)`).

**v1.3.0**: Siparişler sayfası panelin sipariş ekranıyla aynı güçte — kontrol kartları (hazırlanacak, ⚠ etiketi basılmamış, paketi kontrol edilmemiş, maliyeti eksik, kargoda), pazaryeri / tarih / arama / paket kontrol filtreleri, satıra tıklayınca sipariş içeriği (ürünler, kodlar, adet, fiyat, adet başına maliyet), teslimat ve fatura adresi, raftan toplama ve tüm işlem düğmeleri; satır içinde koli / desi ve irsaliye dağılımı (`PUT /orders/{id}`), barkodla paket kontrol (`POST /orders/pack-check`). Ürünler sayfasında pazaryerine özel kod / fiyat düzenleme (`PUT /products/{sku}/links`, `PUT /products/{sku}/prices`, `POST /products/{sku}/resend`). Paket kontrol, toplu stok işlemleri ve aramalarda **kamerayla barkod okuma** (harici kütüphane yok; `BarcodeDetector` yoksa yerleşik EAN-13 / EAN-8 / UPC-A / Code-128 çözücüsü). İrsaliyeler `profile=key` ile basılır: her site kendi logosuyla.

**v1.1.0'dan itibaren panelde ne varsa eklentide de var** ve hiçbir form sayfayı yenilemez (AJAX): Ayarlar sekmeleri — Bağlantı, Senkron, Siparişler & müşteriler, **Gönderici & irsaliye** (siteye özel profil + "WooCommerce'den doldur" + ortam kütüphanesinden logo), **Kesintiler & vergi** (`/settings/tax`, `/settings/web-fees`, `/connections/{id}/fees`), **Kargo & desi** (`/settings/cargo-rates`, `/cargo/carriers`, `/cargo/accounts`), **e-Fatura & e-posta** (`/settings/invoicing` + test düğmeleri), Özellikler & bildirimler, Webhook; ayrı sayfalar — **Pazaryerleri** (bağlantı ekle/düzenle/test/çek/teşhis/sil), **Faturalar** (`/invoices`, tekrar dene / iptal / e-postala + fatura kesilmemiş siparişlere toplu e-Fatura), **Kargo gönderileri** (`/shipments`, sorgula / iptal), Siparişler (toplu e-Fatura, toplu irsaliye, paketle, kargo gönderisi, teslim — `POST /orders/bulk`, `GET /orders/packing-slips`) ve WooCommerce sipariş listesi toplu işlemleri.
Bu API'nin tamamını kullanır: ürün / stok / fiyat / içerik gönderimi (`POST /products`, WooCommerce'te stok değişince anında), pazaryeri siparişlerini WooCommerce siparişi olarak kaydetme (`GET /orders`, webhook), müşteri kaydı (isteğe bağlı, pazaryeri adıyla), sipariş işlemleri (paketle / kargo bildir / teslim / iptal / fatura / gönderi / irsaliye / paket kontrol / raftan toplama), site siparişlerini `POST /orders/web` ile gönderme, stok olayları (`GET /events` + `POST /events/ack`, `stock.changed`), raflama (raflar, mal girişi, rafa yerleştirme, transfer, sarf, kayıp, sayım, iade girişi; ürün sayfasında raf dağılımı ve tüm hareket geçmişi), iadeler (karar + kalem bazlı stoğa alma, raf seçimli), sorular (cevaplama), finans (dönem özeti, sipariş kârlılığı, giderler — WordPress veritabanında saklanır). Kurulum adımları eklenti içindeki `KURULUM.md` dosyasındadır.

### Kendi eklentiniz (OpenCart / Shopify / özel yazılım)
Eklenti tarafında yapılacaklar: ürün/stok değişince `POST /stock` (veya `POST /products`); sitede sipariş oluşunca `POST /orders/web` (`external_id` = site sipariş no);
sipariş durumu/takip numarası için sitenin bir adresine giden webhook (`POST /webhooks` → `order.shipped`, `order.delivered`, `order.cancelled`) ya da periyodik `GET /orders?updated_since=`;
pazaryeri satışlarının site stoğuna yansıması için `GET /events` → mağaza stoğu güncelleme → `POST /events/ack` (ya da `stock.changed` webhook'u). Her eklenti için API sayfasından kanal etiketli ayrı anahtar üretin.
