# Storefront API

> Mağaza sayfasının ve mağaza sunucusunun istek gönderdiği endpoint'ler.

Kaynak: /tr/reference/storefront-api/

Sürüm `v1`. Endpoint yollarının başına eklenecek Sayproof API adresi panelde **Entegrasyon** ekranında gösterilir.

## Kimlik doğrulama

| Çağıran | Yöntem |
|---|---|
| Tarayıcı (mağaza sayfası) | Site anahtarı: okuma isteklerinde `?key=` parametresi, yazma isteklerinde `X-Sayproof-Site-Key` header'ı. İstek **doğrulanmış** bir alan adından gelmelidir: alan adı `Origin` header'ından, `Origin` yoksa `Referer` header'ından okunur. |
| Mağaza sunucusu | `Authorization: Bearer <sunucu anahtarı>` header'ı ve `?channel=<kanal kodu>` parametresi |

Hiçbir endpoint tarayıcı oturumunu (cookie) kullanmaz; istekler cookie olmadan gönderilir.

## Ortak kurallar

- İstek ve yanıt body'leri JSON biçimindedir.
- Hata yanıtının biçimi tüm endpoint'lerde aynıdır: `{"error": "<kod>", "detail": "<kod>"}`. `detail` de bir cümle değil, koddur; kullanıcıya gösterilecek metne kendi tarafınızda dönüştürülür. Kodların listesi için: [Hata kodları](/tr/reference/errors.md).
- Dil sırasıyla body'deki `language` alanından (yazma istekleri), `?lang=tr|en` parametresinden ya da kanalın dilinden belirlenir. `Accept-Language` header'ı dikkate alınmaz. Desteklenmeyen bir dil istenirse kanalın dili kullanılır.
- Her yanıt `X-Request-Id` header'ını taşır; destek talebinde bu değeri iletin.
- Anahtar, kanal ya da alan adıyla ilgili sorunlarda `403 refused` yanıtı döner ve `detail` nedeni belirtir (örneğin `origin_unverified: www.markaniz.com`, `unknown_site_key`, `channel_inactive`). `origin_unverified` ayrıntısı isteğin geldiği alan adını taşır. Rate limit aşıldığında `429 rate_limited` döner.

## Yorum listesi

`GET /v1/reviews/list` — yayındaki yorumları, özeti ve zorunlu öğeleri döndürür.

| Parametre | Varsayılan | Kural |
|---|---|---|
| `key` | | Site anahtarı |
| `product` | | Zorunlu; en fazla 128 karakter |
| `scope` | `variant` | `variant` ya da `group` |
| `sort` | Pazarın varsayılanı | İzin verilen liste dışındaki değer `400 sort_not_allowed` yanıtı alır |
| `page`, `page_size` | `1`, `10` | `page` 1–1000, `page_size` 1–50; aralık dışındaki değerler en yakın sınıra çekilir |
| `rating` | | 1–5; aralık dışındaki değer `400 invalid / rating` yanıtı alır |
| `verified` | | `1`: yalnızca satın alma kanıtı olan yorumlar |
| `media` | | `1`: yalnızca fotoğraf ya da video içeren yorumlar |
| `q` | | Başlık ve metinde arama; en fazla 100 karakter. Tüm kelimeler geçmelidir. Büyük/küçük harf ve ı/i farkı dikkate alınmaz. |
| `topic` | | Tek kelime (en fazla 40 harf); bu kelimeyle başlayan bir kelime içeren yorumlar (`rahat` → `rahatlık`) |
| `lang` | Kanalın dili | `tr` ya da `en` |
| `via` | | Script `script` gönderir. Yorumları kendi kodunuzla okuyorsanız göndermeyin; panelin **Mağaza görünümü** ekranı bu sayfaları sitenizin kendi tasarımı olarak sayar. |

Yanıt:

```json
{
  "reviews": [
    {
      "id": "…", "rating": 5, "title": "…", "body": "…", "author": "Ayşe K.", "anonymous": false,
      "language": "tr", "published_at": "…", "verified": true, "verification_method": "tokenized_invite",
      "helpful_count": 3, "unhelpful_count": 1,
      "aspects": [{ "key": "size", "label": "Beden", "scale": "fit", "value": 2, "position": 0.5, "step_label": "Yeterli" }],
      "aspect_scores": { "size": 2 }, "aspect_template": "footwear",
      "media": [{ "id": "…", "kind": "image", "url": "…", "srcset": "… 320w, … 640w, … 1280w", "width": 1280, "height": 960, "alt_text": "", "poster_url": "" }],
      "seller_response": { "body": "…", "published_at": "…" },
      "resolution": { "body": "…", "created_at": "…" },
      "origin": "own", "origin_market": "", "experience": "product", "translations": {}
    }
  ],
  "total": 200, "page": 1, "pages": 20, "page_size": 10, "sort": "date", "shared": 0,
  "ai_summary": null,
  "sorts": [{ "code": "date", "label": "En yeni" }, { "code": "rating", "label": "Puana göre" }, { "code": "seller", "label": "Satıcı yanıtı olanlar" }],
  "aggregate": { "count": 200, "average": 4.1, "stars_filled": 4, "distribution": { "1": 5, "5": 120 }, "verified_count": 200 },
  "aspects": [{ "key": "size", "label": "Beden", "scale": "fit", "steps": ["Düşük", "Yeterli", "Yüksek"], "average": 2.1, "position": 0.55, "step_label": "Yeterli", "count": 48 }],
  "topics": [{ "word": "rahat", "count": 31 }],
  "gallery": [{ "id": "…", "kind": "image", "url": "…", "srcset": "…", "width": 1280, "height": 960, "alt_text": "", "poster_url": "", "review_id": "…", "rating": 5, "author": "Ayşe K.", "anonymous": false }],
  "media_reviews": 14, "style": "cards", "theme": "warm",
  "applied": { "q": "", "topic": "", "rating": null, "media": false, "verified": false },
  "core": {
    "method_statement": { "url": "…", "label": "Yorumları nasıl topluyoruz", "required": true },
    "sort": { "allowed": ["date", "rating", "seller"], "default": "date", "locked": true },
    "verification_badge": { "label": "Doğrulanmış alıcı", "required": true },
    "source_label_required": true, "streams_together": true, "resolution_inline": true,
    "aggregate": { "…": "…" }, "policy_version": 1, "market": "TR"
  },
  "language": "tr",
  "words": { "verified": "Doğrulanmış alıcı", "…": "…" },
  "report_reasons": [{ "code": "suspected_fake", "label": "…" }],
  "questions_enabled": false,
  "privacy_url": "https://markaniz.com/kvkk-aydinlatma-metni",
  "limits": { "media_count": 6, "title_length": 200, "body_length": 5000, "name_length": 120, "alt_text_length": 300, "image_bytes": 12582912, "video_bytes": 209715200, "video_seconds": 60, "reviews_per_call": 20, "order_reference_length": 128, "email_length": 254, "report_note_length": 1000 }
}
```

- `aggregate.stars_filled`: dolu yıldız sayısı; yarım değerler yukarı yuvarlanır (4,5 → 5).
- `aspects` (üst düzey): basamaklı ölçekli değerlendirme başlıklarının ortalamaları. `position` 0 ile 1 arasındadır, `step_label` en yakın basamağın etiketidir. Filtreler bu değerleri değiştirmez.
- `topics`: yorumlarda en sık geçen en fazla 5 kelime ve her birinin kaç yorumda geçtiği. Kelimeler en az 4 harfli olmalı ve en az 2 yorumda geçmelidir; değerler doğrudan `topic` parametresinde kullanılabilir.
- `gallery`: ürünün yayındaki yorumlarına eklenmiş ve işlenmesi tamamlanmış en fazla 12 fotoğraf ya da video; en yeni yorumdan başlar. Her öğe `media` alanlarını ve ait olduğu yorumun `review_id`, `rating`, `author` ve `anonymous` değerlerini taşır. Filtreler bu listeyi değiştirmez.
- `media_reviews`: fotoğraf ya da video içeren yayındaki yorum sayısı; `media=1` filtresinin tek başına döndürdüğü yorum sayısıdır. Filtreler bu değeri değiştirmez.
- `style`: kanalın panelde seçilen düzeni, `cards` ya da `sidebar`.
- `theme`: kanalın panelde seçilen tasarımı: `warm`, `trust`, `editorial`, `minimal`, `soft`, `bold`, `natural` ya da `night`.
- `applied`: uygulanan filtreler.
- `experience`: `product` (ürün yorumu) ya da `process` (siparişi iptal edilen ya da ürünü iade eden müşterinin o aşamaya kadarki deneyimi). `process` yorumları `words.experience_process` etiketiyle gösterilmelidir; varsayılan bileşen bu etiketi kendisi gösterir.
- `limits`: yorum formunun uyguladığı sınırlar: dosya sayısı (`media_count`), metin uzunlukları (`title_length`, `body_length`, `name_length`, `alt_text_length`), dosya boyutları byte olarak (`image_bytes`, `video_bytes`), video süresi saniye olarak (`video_seconds`), tek istekteki ürün sayısı (`reviews_per_call`), sipariş numarası ve e-posta uzunluğu (`order_reference_length`, `email_length`) ve bildirim notu uzunluğu (`report_note_length`).
- `core.method_statement.url`: isteğin kanalı ve dili için geçerli yöntem beyanının adresi (kanal için bağlanan kendi sayfanız ya da Sayproof'un sayfası).
- Yanıt `Cache-Control: private, max-age=30` header'ını taşır.
- `questions_enabled`: markanın soru-cevabı açık mı; ayrıntılar için [Soru-cevap](#soru-cevap).
- `privacy_url`: isteğin kanalında panelde girilen aydınlatma metni bağlantısı; girilmemişse boş. Script bu adresi yorum, soru ve cevap formlarının altında **Kişisel verilerin işlenmesi hakkında aydınlatma metni** bağlantısı olarak gösterir (`words.privacy_notice`).
- `origin`, `origin_market`: `own` markanın bu pazardaki yorumudur. Markanızda pazarlar arası paylaşım açıksa (**Ayarlar › Yayınlama › Diğer pazarlardan yorumlar**) liste aynı ürünün diğer pazarlarınızdaki satın alma kanıtlı yorumlarını da taşır: bu satırlarda `origin` `syndicated`, `origin_market` geldiği pazardır. Bu yorumlar ortalamaya, puan dağılımına ve yapılandırılmış veriye girmez; `shared` listede kaç tane olduklarını söyler.
- `origin_label`, `origin_url`: kaynağı `own` olmayan her yorumda hazır etiket metni (sunucu, ziyaretçinin dilinde ve doğru Türkçe ekleriyle yazar; script yalnız gösterir). Pazaryerinden aktarılmış yorumda (`origin: "imported"`) etiket "Trendyol'da satın alındı · Trendyol'dan otomatik aktarıldı" gibidir, `origin_url` kaynaktaki yorumun adresidir (yoksa boş) ve `verified` her zaman `false`, `verification_method` boştur. Bu yorumlar `verified_only` süzgecine girmez, ortalamaya, puan dağılımına ve yorum sayısına katılır, yapılandırılmış veriye ve Google ürün yorumu dosyasına girmez. Ayrıntılar için [Pazaryeri yorumları](/tr/features/marketplace-reviews.md).
- `aggregate.imported_count`, `aggregate.imported_label`: aktarılmış yorum sayısı ve özet satırının hazır metni ("4 yorumun 3'ü Trendyol'dan"); aktarılmış yorum yoksa `0` ve boş metin. `aggregate.verified_count` aktarılmış yorumları saymaz.
- `translations` (v2 ile gelecek; şu an her yorumda boş nesne `{}` döner): yapay zekâ paketinde çeviri açıksa yorumun diğer dillerdeki çevirileri, dil koduna göre `{"en": {"title", "body"}}`. Script, yorum ziyaretçinin dilinde değilse çeviriyi gösterir ve aslını gösterme bağlantısı ekler.
- `ai_summary` (v2 ile gelecek; şu an her zaman `null` döner): yapay zekâ paketinde yorum özeti açıksa ve ürünün yeterli yorumu varsa özet: `text`, dayandığı yorum sayısı (`review_count`), oluşturulma zamanı (`created_at`) ve öne çıkan konular (`topics[]`: `topic`, `sentiment`, `count`); yoksa `null`. Varsayılan bileşen özeti etiketiyle birlikte gösterir; özet bölümü gizlenemez.

## Server-side rendering (SSR)

`GET /v1/ssr` — hazır HTML'i, liste yanıtıyla aynı içerikteki `state` alanını ve yapılandırılmış veriyi döndürür. Parametreler: `product`, `channel`, `scope`, `name`, `lang`, `sort`, `style` (`cards` ya da `sidebar`; başka bir değer `400 invalid / style` yanıtı alır), `theme` (`warm`, `trust`, `editorial`, `minimal`, `soft`, `bold`, `natural` ya da `night`; başka bir değer `400 invalid / theme` yanıtı alır). Kullanım ve yanıt alanları için: [Server-side rendering (SSR)](/tr/integrate/server-rendering.md).

- Yanıt `ETag` ve `Cache-Control` header'larını taşır: sunucu anahtarıyla gönderilen istekte `private, max-age=60`, site anahtarıyla gönderilen istekte `public, max-age=60`. `If-None-Match` ile gönderilen istekte içerik değişmemişse `304` döner.
- Kanalın kullanımda bir site anahtarı yoksa `409 refused / no_site_key` döner.

## Oturum açmış müşteri

`GET /v1/viewer` — yalnızca sunucu anahtarıyla kullanılabilir. Müşterinin e-posta adresi `X-Sayproof-Customer` header'ında gönderilir. Parametreler: `product`, `scope`, `lang`, `channel`. Yanıttaki `state` değeri `can_write`, `written` ya da `none` olur; yanıt ayrıca `reviews`, `limits`, müşteri yorum yazabiliyorsa `token` ile `products` ve marka soru-cevabı açtıysa `asker` (`token`, `name`) alanlarını içerir. Soruyla gösterilecek ad isteğe bağlı `X-Sayproof-Customer-Name` header'ıyla, UTF-8 percent-encode edilerek gönderilir. Yanıt `Cache-Control: no-store` header'ını taşır. Ayrıntılar için: [Oturum açmış müşteri](/tr/integrate/signed-in-customers.md).

## Sipariş detayındaki değerlendirme

`GET /v1/viewer/order` — oturum açmış müşterinin sipariş detayı sayfası için siparişin ürünlerini ve her birinin yorum durumunu döndürür. Yalnızca sunucu anahtarıyla kullanılabilir; tarayıcıdan site anahtarıyla gönderilen istek `403 refused / server_only` yanıtı alır. Müşterinin e-posta adresi `X-Sayproof-Customer` header'ında gönderilir. Parametreler: `order` (sipariş numarası, en fazla 128 karakter), `channel`, `lang`.

```json
{
  "order": "100245",
  "token": "…",
  "lines": [
    { "line_reference": "1", "variant_key": "URUN-38", "group_key": "URUN", "title": "Koşu ayakkabısı", "aspects": [{ "key": "size", "label": "Beden", "kind": "product", "required": false, "scale": "fit", "steps": ["Düşük", "Yeterli", "Yüksek"] }], "state": "can_write" },
    { "line_reference": "2", "variant_key": "CORAP-2", "group_key": "CORAP", "title": "Koşu çorabı", "aspects": [], "state": "pending", "rating": 4 }
  ],
  "limits": { "media_count": 6, "body_length": 5000, "…": "…" },
  "language": "tr",
  "theme": "warm",
  "privacy_url": "https://markaniz.com/kvkk-aydinlatma-metni",
  "words": { "order_title": "Ürünleri değerlendirin", "…": "…" }
}
```

- `lines[].state`: `can_write`, `pending`, `published`, `rejected`, `removed`, `waiting` ya da `closed`. Anlamları için: [Oturum açmış müşteri › Sipariş detayı sayfası](/tr/integrate/signed-in-customers.md#siparis-detayi-sayfasi).
- `privacy_url`: kanalın aydınlatma metni bağlantısı; girilmemişse boş. Script bağlantıyı formların altında gösterir.
- `token`: yorum yazılabilen satırlar için tek bir yazma token'ı; yazılabilen satır yoksa boş. `aspects` yalnızca `can_write` satırlarında dolu gelir; `rating` yalnızca yorumu yazılmış satırlarda bulunur.
- Sipariş bulunamazsa ya da başka bir müşteriye aitse yanıt yine `200` olur ve `lines` boş döner; yanıttan siparişin var olup olmadığı anlaşılmaz.
- `order` gönderilmezse `400 invalid / order`, `X-Sayproof-Customer` header'ı eksikse ya da geçerli bir adres değilse `400 invalid / customer` döner.
- Yanıt `Cache-Control: no-store` header'ını taşır. Rate limit `GET /v1/viewer` ile ortaktır.

## "Faydalı" oyu

`POST /v1/reviews/<id>/helpful` — body: `{"vote": "up" | "down" | "none", "visitor_key": "…"}`.

```json
{ "vote": "down", "counted": true, "helpful_count": 3, "unhelpful_count": 1 }
```

- Her ziyaretçi bir yorum için tek oy verebilir. Aynı yönde yeniden oy verilirse `counted: false` döner; yön değiştirilirse oy yeni yöne taşınır; `none` oyu geri alır (`vote: null`).
- `visitor_key` yalnızca tekrarları ayıklamak için kullanılır; rate limit IP adresine göre uygulanır.

## Yorum bildirme

`POST /v1/reviews/<id>/report` — body: `{"reason": "…", "note": "…", "visitor_key": "…"}`. Gerekçe, liste yanıtındaki `report_reasons` alanından seçilir; liste dışındaki değer `400 invalid_reason` yanıtı alır. Not en fazla 1000 karakter olabilir. Başarılı istekte `201 {"id"}` döner. Yalnızca yayındaki yorumlar bildirilebilir. Aynı ziyaretçinin (`visitor_key`, gönderilmezse IP adresi) aynı yoruma ilişkin açık bir bildirimi varsa yeni kayıt oluşturulmaz; yanıt mevcut bildirimin `id` değerini döndürür.

## Ürün görüntülenmesi

`POST /v1/views?key=…` — body, `text/plain` içerik türüyle gönderilen JSON'dur: `{"product": "…", "scope": "variant" | "group"}`. Script ayrıca yorumların nasıl gösterildiğini de bildirir: `shown` (`default` varsayılan bileşen, `coded` görünümü kodda seçilmiş, `headless`), kodda seçildiyse `style` ve `theme`, ve yorum alanının piksel genişliği `width`. Sayproof genişliği yalnızca dört aralıktan birine çevirerek saklar; bu alanlar yoksa ya da geçersizse yalnızca görüntülenme sayılır. Başarılı istekte body içermeyen `204` döner. Script bu isteği kendisi gönderir; headless kullanımda da script sayfada yüklü olduğu sürece ayrıca göndermeniz gerekmez. Otomatik istemcilerden (bot, bağlantı önizleme aracı) ve önceden yükleme (prefetch) isteklerinden gelen görüntülenmeler sayılmaz. Uygulama içi tarayıcılar (örneğin Pinterest, Telegram, Slack, Discord) ve adında "bot" geçen CUBOT gibi telefon markaları sayılır. Önceden hazırlanan (prerender) bir sayfada script, görüntülenmeyi sayfa gerçekten gösterildiğinde bildirir.

## Yöntem beyanı

`GET /v1/method-statement?key=…&lang=tr|en` — herkese açık sayfa; alan adı doğrulaması gerektirmez (yeni sekmede açılır). `format=json` parametresiyle makine tarafından okunabilir biçimde döner. Sayfa, site anahtarının kanalına ait beyanı gösterir; metin o kanalın kurallarından ve yayınlama yönteminden hazırlanır ve panelde o kanal için gösterilen metinle aynıdır.

## Yorum yazma akışının endpoint'leri

Aşağıdaki endpoint'leri script kullanır. Yorum yazma akışı headless kullanımda da script'te kaldığı için bu endpoint'lere doğrudan istek göndermeniz gerekmez.

| Endpoint | İşlevi |
|---|---|
| `POST /v1/eligibility` | Sipariş numarası ve e-posta adresiyle uygunluğu denetler. Müşteri uygunsa tek kullanımlık bir yazma token'ı döner; değilse tek ve genel bir ret yanıtı döner. |
| `POST /v1/eligibility/token` | Davet, alışveriş deneyimi daveti ya da düzeltme bağlantısındaki token'ı doğrular. Yanıttaki `mode` değeri `write`, `feedback` ya da `revise` olur. `feedback` yanıtındaki ürünler değerlendirme başlığı içermez. `revise` yanıtı düzeltilecek yorumu, yazanın girdiği adı (`review.author_name`), adın sitede gizlenip gizlenmediğini (`review.hide_name`) ve yoruma ekli dosyaları (`review.media`: `id`, `kind`) içerir. Bağlantı başka bir pazarın kanalında açılırsa `403 token_refused / token_foreign_market` döner. |
| `POST /v1/media` | Fotoğraf ya da video yükler (jpeg, png, webp: en fazla 12 MB; mp4, mov: en fazla 200 MB ve 60 saniye). Dosya güvenlik taramasından geçirilir; konum ve cihaz bilgileri silinir. Süresi sınırı aşan video yüklenirken reddedilir: `422 refused / video_too_long: 60`. 3840×2160'tan (dikeyde 2160×3840) büyük, saniyede 60 kareden fazla ya da H.264, HEVC, MPEG-4, VP9, AV1 dışında kodlanmış video da reddedilir. Yanıt dosyanın adresini taşımaz; dosya ancak eklendiği yorum yayındayken herkese açık adreste durur. |
| `POST /v1/reviews` | Yorumları gönderir (bir istekte en fazla 20 ürün). Ürün daveti ve alışveriş deneyimi daveti token'larını kabul eder; alışveriş deneyimi daveti token'ıyla yazılan yorum `experience: "process"` olarak kaydedilir. Her satır isteğe bağlı `author_name` (en fazla 120 karakter) ve `hide_name` (`true` ya da `false`; varsayılan `false`) alanlarını taşıyabilir. `hide_name: true` olduğunda ad sitede, yapılandırılmış veride ve Google dosyasında her sözcüğün ilk harfi ile `***` olarak görünür (`Ayşe Kaya` yerine `A*** K***`); adın tamamı yalnızca markanın panelinde görünür. `hide_name` mantıksal değer değilse `400 invalid / hide_name` döner. |
| `POST /v1/reviews/revise` | Reddedilen yorumu düzeltilmiş hâliyle yeniden gönderir. Body'deki `kept_media`, yorumda kalacak mevcut dosyaların kimlik listesidir; alan gönderilmezse mevcut dosyaların hepsi kalır. Kalan ve yeni eklenen dosyalar birlikte en fazla 6 olabilir. `author_name` ve `hide_name` gönderilirse yorumdaki değerlerin yerine geçer (boş `author_name` adı siler); gönderilmezse mevcut değerler korunur. |

Rate limit değerleri için: [Limitler](/tr/reference/limits.md).

## Soru-cevap

Marka panelde **Ayarlar › Yayınlama › Soru-cevap** seçeneğini açtığında kullanılabilir; kapalıyken üç endpoint de `403 refused / questions_disabled` döner ve liste yanıtındaki `questions_enabled` değeri `false` olur. Script bölümü yalnızca bu değer `true` iken yükler.

| Endpoint | İşlevi |
|---|---|
| `GET /v1/questions` | Ürünün yayındaki sorularını ve her sorunun yayındaki cevaplarını döndürür. Parametreler: `key`, `product` (zorunlu), `scope` (`variant` ya da `group`), `page` (1–1000), `page_size` (varsayılan 5, en fazla 20), `lang`. Sunucu anahtarıyla `channel` parametresiyle de okunabilir. |
| `POST /v1/questions` | Ziyaretçinin sorusunu kaydeder. Body: `product`, `scope`, `name` (isteğe bağlı, en fazla 120 karakter), `email` (isteğe bağlı; yoksa cevap geldiğinde e-posta gitmez), `body` (zorunlu, en fazla 1000 karakter), `hide_name` (isteğe bağlı mantıksal değer; `true` ise ad sitede her sözcüğün ilk harfi ile `***` olarak görünür), `language`. Oturum açmış müşteride `name` ve `email` yerine `GET /v1/viewer` yanıtındaki `asker.token` değeri `asker` alanında gönderilir; ad ve e-posta adresi token'dan okunur. Satın alma gerekmez. Başarılı istekte `201 {"id", "state"}` döner. |
| `POST /v1/questions/<id>/answers` | Ürünü satın almış müşterinin cevabını kaydeder. Yazma token'ı `X-Sayproof-Token` header'ında gönderilir; davet ya da sipariş doğrulamasıyla alınan token geçerlidir, yorum yazmak için kullanılmış olması cevap yazmayı engellemez. Body: `body` (zorunlu, en fazla 2000 karakter), `name` (isteğe bağlı), `hide_name` (isteğe bağlı mantıksal değer; `true` ise ad sitede her sözcüğün ilk harfi ile `***` olarak görünür), `language`. Başarılı istekte `201 {"id", "state"}` döner. |

`GET /v1/questions` yanıtı:

```json
{
  "questions": [
    {
      "id": "…", "author": "Deniz", "body": "Geniş ayağa uyar mı?", "published_at": "…",
      "answers": [
        { "id": "…", "kind": "brand", "author": "Örnek Marka", "verified": false, "body": "…", "published_at": "…" },
        { "id": "…", "kind": "buyer", "author": "Ali", "verified": true, "body": "…", "published_at": "…" }
      ]
    }
  ],
  "total": 4, "page": 1, "pages": 1, "page_size": 5,
  "limits": { "question_length": 1000, "answer_length": 2000, "name_length": 120, "email_length": 254 },
  "language": "tr"
}
```

- Sorular pazara göre birleştirilir: aynı pazardaki kanallarda sorulan sorular birlikte gösterilir. En yeni yayınlanan soru önce gelir; cevaplar yayına alınma sırasıyla listelenir.
- `kind`: `brand` (markanın cevabı; `author` kanalın marka adıdır) ya da `buyer` (ürünü satın almış müşterinin cevabı; `verified: true`, "Doğrulanmış alıcı" etiketiyle gösterilir). Sorunun ve cevabın e-posta adresi yanıtta yer almaz.
- Soru ve müşteri cevabı yayına çıkmadan önce onaylanır. Kişisel bilgi, küfür ya da sağlık iddiası içeren metin reddedilmez, onaya düşer. Yayınlama yöntemi Otomatik ya da Karma ise ve metin bu kontrollerden geçerse kendiliğinden yayınlanır.
- Doğrulama eksikse `400 invalid` döner ve `detail` alanı nedeni belirtir: `body_required`, `body_too_long`, `email_invalid`, `email_too_long`, `name_too_long`, `asker_too_long`, `product_required`, `product_too_long`.
- `asker` token'ının süresi (1 saat) dolduysa `403 token_refused / asker_expired`, token geçersizse ya da başka bir markaya aitse `403 token_refused / asker_invalid` döner.
- Token'ın siparişinde sorunun ürünü (aynı varyant ya da aynı ürün grubu) yoksa `403 token_refused / not_a_buyer` döner. Token yoksa `401 token_required`; soru yayında değilse ya da başka pazarınsa `404 not_found` döner.
- Cevap yayına çıktığında, soruda e-posta adresi varsa soruyu soran kişiye markanın adıyla bir e-posta gider. Adres, markanın e-posta almak istemeyenler listesindeyse e-posta gönderilmez.
- `GET /v1/questions` yanıtı `Cache-Control: private, max-age=30` header'ını taşır.

## Örnek istekler

Yorum listesini komut satırından denemek (istek doğrulanmış bir alan adından geliyormuş gibi `Origin` header'ı eklenir):

```bash
curl -s "https://api.sayproof.example/v1/reviews/list?key=SITE-ANAHTARINIZ&product=URUN-38&sort=date&page=1&lang=tr" \
  -H "Origin: https://www.markaniz.com"
```

Tarayıcıdan yorum bildirmek:

```js
export async function reportReview(reviewId, reason, note) {
  const answer = await fetch(`https://api.sayproof.example/v1/reviews/${encodeURIComponent(reviewId)}/report`, {
    method: "POST",
    credentials: "omit",
    headers: { "Content-Type": "application/json", "X-Sayproof-Site-Key": "SITE-ANAHTARINIZ" },
    body: JSON.stringify({ reason, note }),
  });
  if (answer.status === 201) return answer.json();
  const problem = await answer.json().catch(() => ({}));
  throw new Error(problem.detail || problem.error || `sayproof_report_${answer.status}`);
}
```
