# Admin API

> Panelin kullandığı yönetim endpoint'lerine sunucu anahtarıyla istek gönderilmesi; izinler ve ortak liste sözleşmesi.

Kaynak: /tr/reference/management-api/

Panelin kullandığı endpoint'lere (`/admin/v1/…`) sunucudan sunucuya da istek gönderilebilir; örneğin yorumları kendi veri ambarınıza aktarmak ya da denetim raporunu otomatik olarak oluşturmak için.

## Kimlik doğrulama

```http
GET https://api.sayproof.example/admin/v1/reviews?status=published
Authorization: Api-Key sp_<önek>_<gizli>
```

Anahtarın hangi endpoint'leri kullanabileceği **izinlerine** bağlıdır:

| Anahtar izni | Kapsadığı işlemler |
|---|---|
| Yorumları okuma (`read`) | Yorumları, davetleri, gönderim ve bağlantı kayıtlarını ve istatistikleri görüntüleme |
| Yorumlara karar verme (`moderate`) | Okuma işlemleri ile onay, ret ve toplu işlem |
| Dosya indirme (`export`) | Okuma işlemleri ile denetim raporu, işlem geçmişi ve dosya dışa aktarımları (CSV, Excel, JSON) |
| Ayarları değiştirme (`write`) | Okuma işlemleri ve ayarlar. Bu izni yalnızca Sayproof ekibi verebilir. |
| Sayfayı sunucuda hazırlama (`ssr`) | Yalnızca SSR ve oturum açmış müşteri endpoint'leri |
| Sipariş gönderme (`orders`) | Yalnızca sipariş ve teslimat aktarımı endpoint'leri (`/v1/ingest/orders`, `/v1/ingest/deliveries`) |

- Panelden oluşturulan sunucu anahtarları "Sipariş gönderme" (`orders`) ve "Sayfayı sunucuda hazırlama" (`ssr`) izinlerini taşır ve Admin API'yi kullanamaz. Panel oturumunda `POST /admin/v1/channels` isteğine `{"kind": "api_key", "label": "…"}` gönderilip `scopes` verilmediğinde de anahtar bu iki izinle oluşturulur. Panel izin seçtirmez; okuma, karar verme ya da dosya indirme izni olan bir anahtar, panel oturumunda aynı isteğe `scopes` eklenerek oluşturulur (örneğin `"scopes": ["read", "export"]`). Boş liste `400 invalid / scopes_required`, tanınmayan izin `400 invalid / unknown_scope: <izin>` döner. Ayarları değiştirme (`write`) iznini yalnızca Sayproof ekibi verebilir; markanın panelinden istendiğinde `400 invalid / scope_reserved: write` döner.
- **Kullanıcı, kanal, anahtar, kişisel veri talebi ve sipariş bağlantısı endpoint'leri hiçbir anahtarla kullanılamaz**; bu işlemler yalnızca panel oturumunda ve kullanıcının rolüne göre yapılabilir. Böylece ele geçirilen bir anahtarla yeni anahtar oluşturulamaz, kullanıcı eklenemez ve kişisel veri talebi işlenemez. Bu endpoint'lere anahtarla istek gönderildiğinde `403` döner: kullanıcı, kanal, anahtar ve kişisel veri talebi endpoint'lerinde `forbidden / scope_missing`; sipariş bağlantısı endpoint'lerinde anahtarın `write` izni varsa `forbidden / panel_session_required`, yoksa `forbidden / scope_missing`.
- Davet gönderim ayarları (`invite_delivery`, `reminder_delivery`, `webhook_url`, `support_url`, `sender_address`) da yalnızca panel oturumunda değiştirilebilir. `POST /admin/v1/settings` isteği bu alanlardan birini anahtarla değiştirmeye çalışırsa `403 refused / panel_session_required` yanıtı alır; diğer ayarlar değiştirilebilir.
- İzin dışındaki istekler `403`, kimlik bilgisi içermeyen ya da geçersiz anahtarla gönderilen istekler `401` yanıtı alır. Anahtarla yapılan her değişiklik **İşlem geçmişi** ekranına anahtarın adıyla kaydedilir. Kişisel veri gösteren okumalar (yorum ayrıntısı, yorum geçmişi, davet ve mesaj kayıtları, sorular, e-posta adresiyle arama, ret listesi sorgusu) da **İşlem geçmişi** › **Kişisel veri erişimleri** sekmesine anahtarın adıyla yazılır.

## Başlıca endpoint'ler

| Endpoint | İzin | İşlevi |
|---|---|---|
| `GET /admin/v1/reviews` | `read` | Yorum listesi; satırlar katalogdaki ürün adını `product_name` olarak taşır (katalogda yoksa boş). `q` başlıkta, yorum metninde, ürün adında ve kodunda, sipariş numarasında, yazanın adında ve e-posta adresinde arar; tam yorum kimliği verilirse o yorumu döner. `?export=csv`, `?export=xlsx` ya da `?export=json` ile aynı alanları taşıyan CSV, Excel ya da JSON dosyası döner (`export` izni gerekir). Satırlardaki `flag_open`, yorumda karar bekleyen bir otomatik kontrol bulgusu olduğunu söyler; `source`, pazaryerinden aktarılan yorumda `{"platform", "name"}` (örneğin `{"platform": "trendyol", "name": "Trendyol"}`), diğer yorumlarda `null` olur. `entry_point=import` yalnız aktarılan yorumları listeler. |
| `GET /admin/v1/reviews/<id>` | `read` | Yorum ayrıntısı: yazanın girdiği ad (`author`, her zaman tam ad) ve yazan adının sitede gizlenmesini isteyip istemediği (`name_hidden`), satın alma kanıtı, geçmiş, otomatik kontroller, bildirimler ve yapılabilecek işlemler (`available_events`). Fotoğraf ve video adresleri (`media[].url`, `poster_url`, `srcset`) aşağıdaki dosya endpoint'ini gösterir. |
| `GET /admin/v1/reviews/<id>/media/<dosya kimliği>/<dosya adı>` | `read` | Yorumun fotoğraf ya da videosu (özgün dosya, küçük boyutlar, video kapağı). Yorum yayında olmasa da açılır; yanıt önbelleğe alınmaz (`Cache-Control: private, no-store`) ve `Range` header'ı ile parça parça istenebilir. Bilinmeyen dosya ya da başka bir yorumun dosyası `404 not_found` döner. |
| `DELETE /admin/v1/reviews/<id>/media/<dosya kimliği>` | `moderate` | Body `{"reason"}`: yorumun tek bir fotoğrafını ya da videosunu kaldırır. `reason` `personal_data`, `profanity`, `data_subject_request` ve `legal_order` değerlerinden biridir (aynı liste `GET /admin/v1/reasons` yanıtındaki `review_media_removal` altında ve yorum ayrıntısındaki `media_removal_reasons` içindedir); gerekçe yoksa ya da tanınmıyorsa `400 invalid / reason_required` ya da `400 invalid / reason` döner. Dosya hem herkese açık hem kapalı alandan kalıcı olarak silinir; yorumun metni, puanı ve durumu değişmez. Yanıt `204`'tür. Kaldırma yorumun geçmişine ve **İşlem geçmişi**'ne yazılır. Bilinmeyen, daha önce kaldırılmış ya da başka bir yorumun dosyası `404 not_found` döner. |
| `POST /admin/v1/reviews/<id>/transition` | `moderate` | Body `{"event", "reason"}`: onay, ret, gizleme, kaldırma gibi işlemler |
| `GET /admin/v1/moderation/queue` | `moderate` | Onay bekleyen yorumlar; `product_name`, `source` ve `q` araması yorum listesindekiyle aynı. `channel=<kanal kodu>` verilirse liste ve `counters` yalnızca o kanalın yorumlarını kapsar; `counters.elsewhere` (`reviews`, `responses`) diğer kanallarda onay bekleyen yorum ve satıcı yanıtı sayısıdır (`channel` verilmezse ikisi de `0`). |
| `POST /admin/v1/moderation/bulk` | `moderate` | Body `{"ids", "action": "approve" \| "reject", "reason"}`; tek istekte en fazla 200 yorum. Onay beklemeyen yorumlar değiştirilmeden atlanır. Yanıt `{"count", "skipped"}`: işlenen ve atlanan yorum sayısı. |
| `GET /admin/v1/reviews/<id>/trace` | `export` | Yorumun dosyası: `{"review", "timeline"}`. `timeline` satırları yorumun ve satıcı yanıtının adımlarını, bildirimleri, çözüm notunu ve ret bildirimlerini (`subject_type: "notice"`) zaman sırasıyla taşır. `?format=csv\|xlsx\|json` dosya olarak indirir ve **İşlem geçmişi**'ne `review_history.downloaded` yazar. CSV ve Excel'in yorum bölümünde **Panelde aç** satırı paneli bu yorum açık hâlde açan adresi taşır; JSON'da bu alan yoktur. |
| `POST /admin/v1/reviews/<id>/reports/resolve` | `moderate` | Body `{"note"}` (isteğe bağlı, en fazla 1000 karakter): yorumun açık bildirimlerini kapatır. Yanıt `{"closed"}` kapatılan bildirim sayısıdır. |
| `GET /admin/v1/reasons` | `read` | Gerekçe listeleri, istenen dilde; `review_media_removal` tek bir fotoğrafı ya da videoyu kaldırma gerekçelerini taşır |
| `GET /admin/v1/metrics`, `GET /admin/v1/insights` | `read` | İstatistikler. `channel=<kanal kodu>` verilirse sayılar yalnızca o kanalı kapsar; tanınmayan kanal kodu `400 invalid / channel` alır. `GET /admin/v1/insights` isteğinde `market` ile birlikte verilen kanal o pazara ait değilse `400 invalid / channel_market_mismatch` döner. `GET /admin/v1/insights` yanıtındaki `ai` alanı yapay zekâ paketinin dönem özetidir (v2 ile gelecek; şu an `null` döner). |
| `GET /admin/v1/invites`, `GET /admin/v1/send-logs` | `read` | Davetler ve mesaj kayıtları. Davet listesinde `sent=1` yalnızca e-postası gönderilmiş davetleri getirir; `channel=<kanal kodu>` verilirse liste ve `counters` o kanalın siparişleriyle sınırlanır. Mesaj kayıtlarında `q` alıcının adresinde de arar; `channel_kind` (`email` ya da `webhook`; `whatsapp` v2 ile gelecek, şu an bu yolla mesaj gönderilmez) gönderim yoluna göre süzer; `channel=<kanal kodu>` liste ve `counters`'ı o kanalın mesajlarıyla sınırlar (davet siparişin, ret bildirimi yorumun, cevap bildirimi sorunun kanalına göre). Müşterinin kişisel verileri KVKK talebiyle silinmişse `recipient` boş, `recipient_erased` `true` döner. Davetlerin `counters.conversion` değeri gönderilmiş davetlerden en az bir ürünü yorumlanmış olanların oranıdır. |
| `POST /admin/v1/invites/retry` | `write` | "Gönderilemedi" durumundaki davetleri yeniden sıraya alır. Body isteğe bağlıdır: `{"ids": […]}` (en fazla 500 davet) ve `{"channel": "<kanal kodu>"}` (yalnızca o kanalın davetleri); ikisi de gönderilmezse tümü. Bağlantısının süresi dolmuş davetler atlanır. Yanıt `{"queued", "expired"}`. |
| `GET /admin/v1/diagnostics` | `read` | Bağlantı kayıtları |
| `POST /admin/v1/opt-outs/check` | `read` | Body `{"addresses": […]}` (tek istekte en fazla 1.000 adres): listedeki adreslerden e-posta almak istemeyenleri döndürür: `{"opted_out": [{"address", "channel_kind", "reason", "created_at"}]}`. Telefon numarası uluslararası (`+905321112233`) ya da kanallarınızın pazarındaki yerel yazımla (`0532 111 22 33`) verilebilir; yanıtta gönderdiğiniz yazım döner. |
| `GET /admin/v1/opt-outs/export` | `export` | E-posta almak istemeyenler listesinin tamamını indirir; `?format=csv` (varsayılan), `?format=xlsx` ya da `?format=json`. Dosya adresin kendisini değil, küçük harfe çevrilmiş e-posta adresinin SHA-256 değerini taşır (`address_hash`, `channel_kind`, `reason`, `created_at`). WhatsApp satırlarındaki (v2 ile gelecek; şu an liste telefon satırı içermez) telefon numaraları Sayproof'a özgü anahtarlı bir karmayla saklanır ve dosyada bu değerle yer alır; telefon listenizi eşleştirmek için `POST /admin/v1/opt-outs/check` ucunu kullanın. Her indirme **İşlem geçmişi**'ne kaydedilir. |
| `GET /admin/v1/policies` | `read` | Markanın gördüğü pazar kuralı sürümleri (`profiles`; Sayproof kuralları ve markanın kendi sürümleri, `own`, `in_force`, `scheduled`) ve pazar başına sınırlar (`limits`). `channel=<kanal kodu>` verilirse yalnızca o kanalın pazarının ve o kanalın kuralları döner. Yeni sürüm (`POST`) yalnızca panel oturumunda, "Pazar kurallarını yönetebilir" izniyle açılır: [Kuralların güncellenmesi](/tr/features/market-rules.md#kurallarin-guncellenmesi). |
| `GET /admin/v1/method-statement` | `read` | Her açık kanal ve dil için yöntem beyanı satırları (`rows`) ve kendi sayfanız için kullanılabilecek doğrulanmış alan adları (`domains`). Her satır `channel`, `channel_label`, `market`, `language`, `state`, `external_url`, `hosted_url`, `effective_url`, `current_fingerprint`, `confirmed` ve beyan metnini (`statement`) taşır. `?channel=<kanal kodu>&lang=tr` tek kanalın beyanını ve kendi sayfanızın durumunu döner. Tanınmayan kanal kodu `400 invalid / channel` alır. |
| `POST /admin/v1/method-statement`, `POST /admin/v1/method-statement/confirm` | `write` | Kendi sayfanızın adresini kaydeder: body `{"channel", "language", "external_url"}`; boş `external_url` bağlantıyı Sayproof sayfasına döndürür. Onay: body `{"channel", "language", "fingerprint"}`; `fingerprint` satırdaki `current_fingerprint` değeridir, metin bu arada değiştiyse `409 statement_changed` döner. Tanınmayan kanal kodu `400 invalid / channel` alır. |
| `POST /admin/v1/audit-file` | `export` | Body `{"market", "channel", "start", "end"}`: denetim raporunu oluşturur ve saklar. `market` ve `channel` boşsa rapor bütün kanalları, yalnız `market` verilirse o pazarın bütün kanallarını, `channel` verilirse yalnızca o kanalı kapsar; pazar kanaldan alınır. Tanınmayan kanal kodu `400 invalid / channel`, kanalın pazarıyla uyuşmayan `market` `400 invalid / channel_market_mismatch` alır. Saklanan rapor ve dosya adı kanal kodunu taşır; önceki raporlar listesi `channel` filtresiyle süzülür. |
| `GET /admin/v1/audit-file/<id>?format=json\|csv\|xlsx` | `export` | Saklanan raporu indirir; `X-Sayproof-Fingerprint` header'ı doğrulama kodudur. CSV ve Excel'de kaldırılan ve gizlenen yorumların satırlarında **Panelde aç** sütunu bulunur; Excel'de adresler tıklanabilir. |
| `GET /admin/v1/audit-log` | `export` | İşlem geçmişi. Paneldeki **İndir** menüsü dosyayı `POST /admin/v1/audit-log/export` ile Sayproof'a ürettirir: body `{"format", "title", "filters", "columns", "labels", "changes"}`. Satırlar sunucuda, listedeki filtrelerin (`filters`) aynısıyla bu kayıtlardan okunur; en fazla 10.000 kayıt. `columns` sekiz sütunun adını, `labels` işlem, rol ve hedef kodlarının ekrandaki adlarını (`action`, `role`, `target`) ve `system` sözcüğünü, `changes` kayıt numarasına göre **Ne değişti** metnini taşır; son sütun işlemin kodudur. İndirme aynı istekte **İşlem geçmişi**'ne `audit_log.downloaded` işlemi olarak biçimi, satır sayısı ve dosyanın SHA-256 hash'iyle (`fingerprint`) kaydedilir. Bozuk istek `400 invalid / rows`, 10.000'i aşan kayıt `400 invalid / rows_over_limit` döner. |
| `GET /admin/v1/access-log` | `export` | Kişisel veri erişimleri (**İşlem geçmişi** › **Kişisel veri erişimleri**). Satırlar `action` (`review.viewed`, `reviews.searched`, `review_history.viewed`, `invites.listed`, `send_logs.listed`, `questions.listed`, `question.viewed`, `subject_requests.listed`, `subject_request.viewed`, `subject_request.previewed`, `users.listed`, `opt_out.checked`, `imported_reviews.listed`), `actor_id`, `actor_role`, `via` (`session`, `api_key`, `support`, `console`), `target_type`, `target_id`, `count` (o okumada dönen ve kişisel veri taşıyan kayıt sayısı) ve `at` taşır. Süzgeçler `action`, `actor`, `via`; `q` kişide ve kayıt numarasında arar. Kayıtlar değiştirilemez ve 365 gün sonra silinir. |
| `GET\|POST /admin/v1/invite-templates` | `write` | Davet e-postası şablonları. Satırlar `channel` alanını taşır (kanal kodu; tüm kanallar için hazırlanmış şablonda boş). Kayıtta `channel` verilirse şablon yalnız o kanalın siparişlerinde kullanılır ve `market` kanaldan alınır; tanınmayan kanal kodu `400 invalid / channel` alır. |
| `GET /admin/v1/questions` | `read` | Soru listesi. Süzgeçler: `state`, `market`, `channel`, `unanswered=1`, `answers_waiting=1`; `q` soru metninde, soranın adında ve e-posta adresinde, ürün kodunda arar. Yanıt ayrıca `counters` (`waiting`, `unanswered`, `answers_waiting`) ve markanın soru-cevabı açık mı (`enabled`) bilgisini taşır. |
| `GET /admin/v1/questions/<id>` | `read` | Soru ayrıntısı: soranın adı (`asker_name`, tam ad) ve adının sitede gizli kalıp kalmayacağı (`name_hidden`), cevaplar (her biri `author_name` ve `name_hidden` taşır), her cevabın soran kişiye giden e-postasının sonucu (`notice`), yapılabilecek işlemler (`available_events`) ve geçmiş (`ledger`) |
| `POST /admin/v1/questions/<id>/transition`, `POST /admin/v1/answers/<id>/transition` | `moderate` | Body `{"event", "reason"}`: onay, ret, yeniden inceleme, gizleme ve yeniden gösterme |
| `POST /admin/v1/questions/<id>/answer` | `moderate` | Body `{"body"}`: yayındaki soruya marka cevabı yazar ve hemen yayınlar; yanıt `201` ile cevabın satırıdır. Cevap denetimden geçmezse `409 answer_refused` (`text_steering`, `text_health_claim`, `text_personal_data`, `text_profanity`, `question_not_published`). |
| `POST /admin/v1/reviews/<id>/seller-response/draft` | `moderate` | Yapay zekâ paketiyle satıcı yanıtı taslağı (v2 ile gelecek; şu an `409 seller_response_refused` döner, `detail` `ai_package_off` ya da `ai_feature_closed`): body `{"tone"}` isteğe bağlı (`formal` ya da `warm`; verilmezse markanın varsayılan tonu), yanıt `{"draft"}`. Taslak kaydedilmez. |
| `GET /admin/v1/ai` | `read` | Yapay zekâ paketinin durumu (v2 ile gelecek; şu an `404` döner). Paket talebi `POST /admin/v1/ai/requests` de v2 ile gelecek; şu an `404` döner. |
| `GET\|POST /admin/v1/settings` | `write` | Ayarlar. Yanıttaki `time_zone`, gönderim saatlerinin uygulandığı saat dilimidir (örneğin `Europe/Istanbul`). Diğer alanlar arasında `questions_enabled`, `reply_tone` (`formal` ya da `warm`), `ai_translation_enabled`, `ai_summary_enabled` (bu üçü v2 ile gelecek; şu an kaydedilir ama etkisi yoktur), `syndication_enabled`, `reminder_consent` (`not_required` ya da `required`) ve kurulumda seçilebilen gönderim yolları (`deliveries`) bulunur. Gönderim yolu alanları (`invite_delivery`, `reminder_delivery`, `webhook_url`, `support_url`, `sender_address`) yalnızca panel oturumunda değiştirilir; anahtarla `403 refused / panel_session_required`. Kurulumda kapalı bir yol seçilirse `400 invalid / invite_delivery_unavailable` ya da `reminder_delivery_unavailable`. |

## Kişisel veri talepleri

Kişisel veri talebi endpoint'leri hiçbir anahtarla kullanılamaz; yalnızca panel oturumunda ve "KVKK taleplerini yürütebilir" izni olan kullanıcıyla çalışır. Talep kaydı müşterinin e-posta adresini düz olarak saklamaz; erişim talebinde hazırlanan verilerin kopyası da saklanmaz.

Talep satırları (`GET /admin/v1/subject-requests` listesindeki satırlar ve talep yanıtları) `can_copy` alanını taşır. Değer `true` ise talep tamamlanmış bir erişim talebidir ve kopyası yeniden alınabilir.

`POST /admin/v1/subject-requests/<id>/copy`, tamamlanmış bir erişim talebinin kopyasını güncel kayıtlardan yeniden hazırlar. Body'de talepteki müşterinin e-posta adresi gönderilir:

```http
POST https://api.sayproof.example/admin/v1/subject-requests/<id>/copy
Content-Type: application/json

{"identifier": "ayse@ornek.com", "format": "xlsx"}
```

`format` gönderilmezse yanıt JSON gövdesidir; `csv`, `xlsx` ya da `json` gönderilirse aynı içerik dosya olarak döner (CSV ve Excel düzeni: [Erişim dosyası](/tr/features/personal-data.md#erisim-dosyasi)).

| Yanıt | Anlamı |
|---|---|
| `200` | Talebin bilgileri (`id`, `kind`, `state`, `received_at`, `completed_at`, `affected_reviews`, `purged_media`) ile `export` ve `export_details` alanları. Talebe yazılan not ve talebi işleyen kullanıcı kopyaya girmez; içerik için: [Erişim dosyası](/tr/features/personal-data.md#erisim-dosyasi). |
| `400 invalid / identifier_required` | `identifier` gönderilmemiş ya da boş |
| `400 identifier_mismatch` | Adres, talep kaydındaki adresle eşleşmiyor; kopya verilmez |
| `400 invalid / format` | `format` değeri `csv`, `xlsx` ya da `json` değil |
| `404 not_found` | Bu kimlikte bir talep yok |
| `409 not_copyable` | Talep tamamlanmış bir erişim talebi değil |

Her başarılı istek **İşlem geçmişi**'ne `subject_request.copied` işlemiyle kaydedilir; ekranda "KVKK erişim talebinin kopyası indirildi" olarak görünür.

Her talebin kaydı `GET /admin/v1/subject-requests/<id>` ile okunur; `?format=csv`, `?format=xlsx` ya da `?format=json` kaydı dosya olarak indirir. Bu indirme **İşlem geçmişi**'ne `subject_request.downloaded` işlemiyle ("KVKK talebinin kaydı indirildi") yazılır.

## Sipariş bağlantıları

Akinon, Shopify ve WooCommerce gibi platformlardan sipariş okuyan bağlantıların endpoint'leri hiçbir anahtarla kullanılamaz; yalnızca panel oturumunda ve "Ayarları değiştirebilir" izni olan kullanıcıyla çalışır. Paneldeki karşılığı ve okuma kuralları: [Akinon siparişleri](/tr/integrate/akinon.md), [Shopify siparişleri](/tr/integrate/shopify.md), [WooCommerce siparişleri](/tr/integrate/woocommerce.md).

| Endpoint | İşlevi |
|---|---|
| `GET /admin/v1/order-sources` | Bağlantılar (`items`) ve hazır bağlantısı olan platformlar (`platforms`; her platformun bağlantı alanları `fields` içinde) |
| `POST /admin/v1/order-sources` | Bağlantı kurar. Body: `platform`, `channel` ve platformun bağlantı alanları (Akinon: `base_url`, `remote_channel`, `username`, `password`; Shopify: `base_url`, `client_secret`, isteğe bağlı `client_id`; WooCommerce: `base_url`, `consumer_key`, `consumer_secret`); isteğe bağlı `backfill_days` (1–365, varsayılan 30), `interval_minutes` (5–1440, varsayılan 15) ve `enabled` |
| `POST /admin/v1/order-sources/check` | Kaydetmeden dener. Body kurulumla aynıdır, `channel` gerekmez. |
| `GET\|PATCH\|DELETE /admin/v1/order-sources/<id>` | Bağlantıyı gösterir, değiştirir (`enabled`, `interval_minutes`, `backfill_days` ve platformun bağlantı alanları; giriş bilgileri yalnızca gönderilirse değişir, `base_url` başka bir adrese değişirse giriş bilgileri de yeniden gönderilmelidir) ya da kaldırır |
| `POST /admin/v1/order-sources/<id>/check` | Kayıtlı bilgilerle dener |
| `POST /admin/v1/order-sources/<id>/run` | Okumayı hemen başlatır ve `202` döner; duraklatılmış bağlantıda `409 source_inactive`, kanal kapalıyken `409 channel_inactive` |

Yanıtlarda giriş bilgileri (parola, Client secret, Consumer key ve Consumer secret gibi) hiçbir zaman yer almaz; giriş bilgisi olmayan alanlar `values` içinde döner, `has_credentials` alanı kayıtlı giriş bilgisi olup olmadığını gösterir. `channel_active` bağlantının kanalının açık olup olmadığını bildirir; kanal kapalıyken bağlantı okuma yapmaz. Deneme yanıtı müşteri e-postasını içermez, yalnızca var olup olmadığını (`has_email`) bildirir:

```json
{
  "orders": 3,
  "complete": true,
  "skipped": {"status_unknown": 1},
  "sample": [
    {"external_id": "100234", "placed_at": "2026-09-28T10:12:00+00:00", "payment_state": "paid", "lines": 2, "states": ["delivered"], "has_email": true}
  ]
}
```

`skipped` okunamayan siparişleri nedene göre sayar: `order_incomplete` (numarası ya da tarihi eksik), `status_unknown` (durumu tanınmıyor), `line_incomplete` (ürün satırında ürün kodu eksik), `order_without_lines` (okunabilen ürün satırı yok) ve yalnızca Shopify'da `lines_truncated` (50'den fazla ürün satırı var; ilk 50 satır okundu). Bağlantının son okuma özetinde (`last_report.skipped`) Sayproof'a kaydedilemeyen siparişler `order_refused` ile sayılır.

## Pazaryeri yorumları

Pazaryerlerinden yorum alan bağlantıların endpoint'leri hiçbir anahtarla kullanılamaz; yalnızca panel oturumunda ve "Ayarları değiştirebilir" izni olan kullanıcıyla çalışır. Yorumları listeleyen ve kaldıran iki endpoint de hiçbir anahtarla kullanılamaz; yalnızca panel oturumunda ve "KVKK taleplerini yürütebilir" izni olan kullanıcıyla çalışır. Panelde ve kuralları: [Pazaryeri yorumları](/tr/features/marketplace-reviews.md).

| Endpoint | İşlevi |
|---|---|
| `GET /admin/v1/marketplace/sources` | Bağlantılar (`items`; her biri `last_run`, `runs` ve `attention` taşır; `attention` kaldırması bekletilen bağlantıda `held`, üst üste okunamayan bağlantıda `failed` türünü, sayıyı ve son hata kodunu verir, sorun yoksa `null` olur), pazaryerleri (`platforms`) ve alış yolları (`routes`) |
| `POST /admin/v1/marketplace/sources` | Bağlantı kurar. Body: `platform` (`trendyol`, `hepsiburada`, `amazon`, `n11`, `ciceksepeti`), `channel`, `seller_id`, `route` (`file` ya da `feed`); `feed` yolunda `feed_url` (`https`) ve isteğe bağlı `secret`. Kurulum kimin yaptığıyla **İşlem geçmişi**'ne yazılır. Aynı kanal, pazaryeri ve satıcı numarası için ikinci bağlantı `409 source_exists` alır. |
| `GET\|PATCH\|DELETE /admin/v1/marketplace/sources/<id>` | Bağlantıyı gösterir, değiştirir (`enabled`; `feed` yolunda `feed_url` ve `secret`; `seller_id` değiştirilemez, `400 seller_id_locked`) ya da kaldırır (yayında aktarılmış yorum ya da kişisel veri talebiyle kaldırılmış yorum kaydı varsa `409 source_has_reviews`). `secret` yanıtlarda dönmez; `has_secret` kayıtlı olup olmadığını gösterir. |
| `POST /admin/v1/marketplace/sources/<id>/runs` | Okumayı başlatır ve `202` döner. `file` yolunda `multipart/form-data` ile `file` (CSV, `.xlsx` ya da JSON; en çok 20 MB, 100.000 satır) gönderilir; dosya tam bir karşılaştırmadır. `feed` yolunda body: `scope` (`new` ya da `full`, varsayılan `full`). İkisinde de `confirm_removals` (`true` ise bekletilen kaldırmalar uygulanır). Başka okuma sürerken `409 run_in_progress`, duraklatılmış bağlantıda ya da kapalı kanalda `409 source_inactive`. Dosya okunamazsa `400` ve `detail` içinde `column_missing: <sütun>`, `file_unreadable`, `unsupported_format`, `too_large` ya da `too_many_rows`. |
| `GET /admin/v1/marketplace/sources/<id>/runs` | Son okumalar; her okumanın sayıları ve `report` içinde eşleşme raporu |
| `POST /admin/v1/marketplace/sources/<id>/check` | Hiçbir şey kaydetmeden dener: `file` yolunda `file` gönderilir, `feed` yolunda adres okunur. Yanıt: `rows`, `eligible`, `would_import`, `would_update`, `would_remove`, `matched_barcodes`, `unmatched_barcodes`, `unmatched`, `skips`. |
| `GET /admin/v1/marketplace/imported-reviews` | Aktarılmış yorumları listeler; `q` yorum metninde, yazar adında ve pazaryeri yorum numarasında arar, `platform` süzer. |
| `POST /admin/v1/marketplace/removals` | Seçilen aktarılmış yorumları kişisel veri talebiyle kaldırır. Body: `reviews` (aktarılmış yorum kimlikleri, en çok 100), isteğe bağlı `note`. Yanıt `201`: `id`, `reviews`, `media_purged`, `state`. Talep **Kişisel veri talepleri** listesine silme olarak yazılır. |

Okuma satırında `state` `queued`, `running`, `succeeded`, `held` (kaldırmalar bekletildi) ya da `failed` olur; `error` başarısız okumanın kodudur. `report` `matched_barcodes`, `unmatched_barcodes`, `unmatched` (`barcode`, `reason`: `no_product`, `no_barcode` ya da `ambiguous`, `reviews`), `skips`, `photos_missing` ve bekletilmiş okumada `held_products` taşır.

## Ortak liste sözleşmesi

Liste endpoint'leri aynı parametreleri ve aynı yanıt biçimini kullanır.

Parametreler: endpoint'te tanımlı her filtrenin adı, `q` (arama), `from` ve `to` (tarih ya da tam zaman; yalnızca tarih verilirse o günün tamamı dahil edilir), `sort`, `page` ve `page_size` (en fazla 100).

```json
{
  "items": [],
  "total": 0, "page": 1, "pages": 1,
  "facets": { "state": [{ "value": "sent", "count": 12 }] },
  "filters": [{ "name": "state", "group": "invite_state", "faceted": true, "advanced": false, "kind": "value" }],
  "sorts": ["oldest", "recent"]
}
```

- `filters`, endpoint'in kabul ettiği filtreleri listeler. `kind` filtrenin türünü belirtir: `value` (değer listesi), `flag` (`=1`) ya da `choice` (adlandırılmış seçenekler).
- `facets`, her filtrenin değerlerini o filtrenin kendisi hariç tutularak sayar.

Yanıtlardaki değerler kod olarak döner (`published`, `profanity` vb.). Ekranda göstermek için gerekçe ve durum adlarını `GET /admin/v1/reasons` yanıtı ve kendi çeviri dosyalarınızla metne dönüştürün.

## Örnekler

### Yayındaki yorumları sayfa sayfa almak (Python)

```python
import os

import requests

BASE = os.environ["SAYPROOF_BASE"]
HEADERS = {"Authorization": f"Api-Key {os.environ['SAYPROOF_ADMIN_KEY']}", "Accept-Language": "tr"}


def published_reviews(since):
    page = 1
    while True:
        answer = requests.get(
            f"{BASE}/admin/v1/reviews",
            params={"status": "published", "from": since, "sort": "oldest", "page": page, "page_size": 100},
            headers=HEADERS,
            timeout=30,
        )
        answer.raise_for_status()
        body = answer.json()
        yield from body["items"]
        if page >= body["pages"]:
            return
        page += 1


for review in published_reviews("2026-09-01"):
    print(review["id"], review["rating"])
```

### Denetim raporu oluşturup doğrulamak (Node.js)

```js
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";

const base = process.env.SAYPROOF_BASE;
const headers = { Authorization: `Api-Key ${process.env.SAYPROOF_EXPORT_KEY}`, "Content-Type": "application/json" };

const created = await fetch(`${base}/admin/v1/audit-file`, {
  method: "POST",
  headers,
  body: JSON.stringify({ market: "TR", start: "2026-09-01", end: "2026-09-30" }),
}).then((answer) => answer.json());

const download = await fetch(`${base}/admin/v1/audit-file/${created.copy.id}?format=json`, { headers });
const content = Buffer.from(await download.arrayBuffer());
const fingerprint = download.headers.get("X-Sayproof-Fingerprint");

if (createHash("sha256").update(content).digest("hex") !== fingerprint) {
  throw new Error("audit_file_fingerprint_mismatch");
}
await writeFile(`denetim-${created.copy.id}.json`, content);
```

JSON dosyasının SHA-256 değeri doğrulama koduyla aynıdır; dosyanın indirildikten sonra değiştirilmediğini bu karşılaştırmayla gösterebilirsiniz.

### E-posta almak istemeyen adresleri sorgulamak (Python)

Kendi e-posta gönderimlerinizden önce, Sayproof'taki listede yer alan adresleri ayıklamak için adresleri 1.000'erli gruplar hâlinde sorgulayın. Yanıt yalnızca listede bulunan adresleri döndürür.

```python
import os

import requests

BASE = os.environ["SAYPROOF_BASE"]
HEADERS = {"Authorization": f"Api-Key {os.environ['SAYPROOF_ADMIN_KEY']}"}


def opted_out(addresses):
    found = set()
    for start in range(0, len(addresses), 1000):
        answer = requests.post(
            f"{BASE}/admin/v1/opt-outs/check",
            json={"addresses": addresses[start : start + 1000]},
            headers=HEADERS,
            timeout=30,
        )
        answer.raise_for_status()
        found |= {row["address"] for row in answer.json()["opted_out"]}
    return found
```
