# Sipariş ve teslimat aktarımı

> Sipariş ve teslimat bilgisinin sunucu anahtarıyla Sayproof'a gönderilmesi.

Kaynak: /tr/integrate/orders/

Sayproof yorum yazma hakkını ve davetleri siparişlere dayanarak oluşturur. Bu nedenle mağaza sisteminizin siparişleri ve teslimatları Sayproof'a göndermesi gerekir. Aktarım sunucudan sunucuya yapılır ve her istek **sunucu anahtarıyla** gönderilir; böylece bilginin gerçekten sizden geldiği doğrulanır.

:::note
Mağazanız Akinon, Shopify ya da WooCommerce üzerindeyse kod yazmanız gerekmez: Sayproof siparişleri platformdan kendisi okur. Bkz. [Akinon siparişleri](/tr/integrate/akinon.md), [Shopify siparişleri](/tr/integrate/shopify.md), [WooCommerce siparişleri](/tr/integrate/woocommerce.md). Diğer platformlarda siparişler bu sayfadaki endpoint'ler üzerinden aktarılır; mağaza platformunuzdaki sipariş event'lerini (sipariş oluşturuldu, ödendi, kargoya verildi, teslim edildi, iade edildi) bu endpoint'lere ileten küçük bir entegrasyon servisi yeterlidir.
:::

## Gönderim zamanı

- Sipariş oluşturulduğunda ve ödeme durumu değiştiğinde (ödendi, iade edildi, iptal edildi).
- Bir sipariş satırı kargoya verildiğinde, teslim edildiğinde, iade ya da iptal edildiğinde.

Aynı siparişin yeniden gönderilmesi güvenlidir: sipariş güncellenir, kopyası oluşmaz. En basit yöntem, her değişiklikte siparişin tamamını yeniden göndermektir.

Davet gönderilebilmesi ve yorum yazılabilmesi için sipariş satırının **teslim edilmiş**, siparişin **ödenmiş** olması gerekir. Teslimattan sonra Sayproof, **Davet gönderimi** ekranındaki bekleme süresi (varsayılan 2 gün) dolduğunda daveti gönderir.

## Endpoint'ler

| Endpoint | İşlev |
|---|---|
| `POST /v1/ingest/orders` | Siparişleri ve sipariş satırlarını kaydeder ya da günceller |
| `POST /v1/ingest/deliveries` | Sipariş satırlarının teslimat durumlarını bildirir |

Adresler panelde **Entegrasyon › Sipariş aktarımı** sekmesinin **Mağazadan gönderim** bölümündeki **Siparişleri ve teslimatları gönderin** kartında **Kurulumda kullanılacak değerler** listesinde **Siparişler** ve **Teslimatlar** satırlarında yer alır; aynı listede **Kanal kodu** da bulunur.

## Sunucu anahtarı

Her istek sunucu anahtarını `Authorization` header'ında taşır:

| Header | Değer |
|---|---|
| `Authorization` | `Bearer <sunucu-anahtarı>` |
| `Content-Type` | `application/json` |

Kurallar:

- Sunucu anahtarı **Kanallar › Anahtarlar** sekmesindeki **Sunucu anahtarları** kartında ya da kanal kurulum sihirbazının **Anahtarlar** adımında oluşturulur. [SSR](/tr/integrate/server-rendering.md) için kullandığınız anahtarla aynı anahtar olabilir.
- Anahtarın sipariş gönderme (`orders`) izni olmalıdır. Panelden oluşturulan sunucu anahtarları bu izni kendiliğinden taşır.
- Anahtar yalnızca sunucuda tutulur; tarayıcıya ya da sayfa koduna konmaz.
- Siparişin kanalı body'deki `channel` alanından belirlenir.

### Node.js

```js
export async function sendOrders(orders) {
  const answer = await fetch(`${process.env.SAYPROOF_BASE}/v1/ingest/orders`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.SAYPROOF_SERVER_KEY}`,
    },
    body: JSON.stringify({ channel: "web", orders }),
  });
  if (!answer.ok) throw new Error(`sayproof_ingest_${answer.status}: ${await answer.text()}`);
  return answer.json();
}
```

### Python

```python
import json
import os

import requests


def send_orders(orders):
    body = json.dumps({"channel": "web", "orders": orders}, ensure_ascii=False).encode("utf-8")
    answer = requests.post(
        f"{os.environ['SAYPROOF_BASE']}/v1/ingest/orders",
        data=body,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {os.environ['SAYPROOF_SERVER_KEY']}",
        },
        timeout=10,
    )
    answer.raise_for_status()
    return answer.json()
```

### PHP

```php
function sayproof_send_orders(array $orders): array
{
    $body = json_encode(['channel' => 'web', 'orders' => $orders], JSON_UNESCAPED_UNICODE);
    $curl = curl_init(getenv('SAYPROOF_BASE') . '/v1/ingest/orders');
    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Authorization: Bearer ' . getenv('SAYPROOF_SERVER_KEY'),
        ],
    ]);
    $answer = curl_exec($curl);
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    curl_close($curl);
    if ($answer === false || $status !== 200) {
        throw new RuntimeException('sayproof_ingest_' . $status);
    }
    return json_decode($answer, true);
}
```

### Komut satırı

```bash
curl -X POST "$SAYPROOF_BASE/v1/ingest/orders" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $SAYPROOF_SERVER_KEY" \
  --data '{"channel":"web","orders":[]}'
```

## İmza

İmzalı gönderim isteğe bağlıdır. İsteklerini sunucu anahtarı yerine ortak bir şifreyle imzalamak isteyen ekipler için **Kanallar › Anahtarlar** sekmesinin altındaki **Gelişmiş: imzalı gönderim şifreleri** bölümünde **İmzalı gönderim şifresi** oluşturulur. İstekte `Authorization: Bearer` header'ı varsa Sayproof sunucu anahtarını, yoksa imzayı denetler. İmzalı her istek üç header taşır:

| Header | Değer |
|---|---|
| `X-Sayproof-Secret` | İmzalı gönderim şifresinin **kimliği**: **İmzalı gönderim şifreleri** tablosunun **Şifre kimliği** sütunu. Şifrenin adı gönderilmez. |
| `X-Sayproof-Timestamp` | İsteğin zamanı; saat dilimi içeren ISO 8601 biçiminde: `2026-09-28T07:30:00Z` ya da `2026-09-28T10:30:00+03:00` |
| `X-Sayproof-Signature` | `hex(HMAC-SHA256(şifre, zaman + "." + body))` |

Kurallar:

- İmza, gönderilen **byte'ların kendisi** üzerinden hesaplanır. İmzaladıktan sonra JSON'u yeniden biçimlendirmeyin ve boşluk eklemeyin.
- İstek zamanı Sayproof'un saatinden en fazla 5 dakika farklı olabilir; sunucunuzun saati senkronize olmalıdır.
- Saat dilimi içermeyen zaman değerleri (`2026-09-28T10:30:00`) reddedilir.
- Şifrenin kendisi hiçbir istekte gönderilmez; yalnızca imzayı hesaplamak için kullanılır.

```js
import { createHmac } from "node:crypto";

export async function sendOrdersSigned(orders) {
  const body = JSON.stringify({ channel: "web", orders });
  const timestamp = new Date().toISOString();
  const signature = createHmac("sha256", process.env.SAYPROOF_INGEST_SECRET)
    .update(`${timestamp}.${body}`)
    .digest("hex");
  const answer = await fetch(`${process.env.SAYPROOF_BASE}/v1/ingest/orders`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Sayproof-Secret": process.env.SAYPROOF_INGEST_SECRET_ID,
      "X-Sayproof-Timestamp": timestamp,
      "X-Sayproof-Signature": signature,
    },
    body,
  });
  if (!answer.ok) throw new Error(`sayproof_ingest_${answer.status}: ${await answer.text()}`);
  return answer.json();
}
```

## Sipariş verisi

Tek bir sipariş ya da en fazla 100 siparişten oluşan bir liste gönderilebilir:

```json
{
  "channel": "web",
  "orders": [
    {
      "external_id": "ORD-1001",
      "payment_state": "paid",
      "market": "TR",
      "language": "tr",
      "placed_at": "2026-09-25T10:00:00+03:00",
      "customer_email": "ayse@ornek.com",
      "customer_key": "musteri-4821",
      "items": [
        {
          "line_reference": "1",
          "variant_key": "URUN-38",
          "group_key": "URUN",
          "title": "Koşu ayakkabısı",
          "quantity": 1,
          "fulfilment_state": "delivered",
          "delivered_at": "2026-09-27T09:00:00+03:00"
        }
      ]
    }
  ]
}
```

| Alan | Zorunlu | Açıklama |
|---|---|---|
| `channel` | Hayır | Kanal kodu (en fazla 32 karakter). Body'nin en üstünde verilirse istekteki tüm siparişlere, siparişin içinde verilirse yalnızca o siparişe uygulanır; ikisi birden verilmişse siparişteki değer geçerlidir. Hiçbiri verilmezse siparişin mevcut kanalı korunur. |
| `external_id` | Evet | Sipariş numarası (en fazla 128 karakter). Müşterinin sipariş doğrulamasında gireceği numaradır; büyük/küçük harfe duyarlıdır. |
| `payment_state` | Evet | `pending`, `paid`, `partially_refunded`, `refunded`, `voided`. Yorum ve davet için `paid` ya da `partially_refunded` gerekir. |
| `market` | Hayır | Pazar kodu; örneğin `TR`. Belirtilmezse kanalın pazarı kullanılır. Kanalın pazarından farklı bir pazar gönderilirse sipariş reddedilir (`market_channel_mismatch: <pazar>`). |
| `language` | Hayır | `tr` ya da `en`. Belirtilmezse kanalın dili kullanılır. |
| `placed_at` | Evet | Sipariş zamanı; saat dilimiyle birlikte. |
| `customer_email` | Hayır | Siparişteki e-posta adresi (en fazla 254 karakter). Davet bu adrese gönderilir; sipariş doğrulamasında ve oturum açmış müşteri sorgusunda bu adres kullanılır. Adres gönderilmezse davet gönderilmez. |
| `customer_key` | Hayır | Müşterinin sizin sisteminizdeki kimliği (en fazla 128 karakter). Günlük davet sınırı ve seri gönderim kontrolü bu kimliğe göre, kimlik yoksa e-posta adresine göre uygulanır. |
| `customer_phone` | Hayır | Siparişteki telefon numarası (en fazla 32 karakter); alan kabul edilir. Numara [WhatsApp ile davet](/tr/features/invitations.md#whatsapp-ile-davet) için kullanılacak. WhatsApp ile davet v2 sürümüyle gelene kadar telefon numarası saklanmaz. Sonra numara uluslararası biçimde saklanacak (`+905321112233`); ülke kodu olmayan numara kanalın pazarına göre tamamlanacak (Türkiye pazarında `0532 111 22 33` → `+905321112233`), tamamlanamayan numara saklanmayacak. |
| `items[]` | Evet | Sipariş satırları. |

Siparişteki e-posta adresi, telefon ve müşteri numarası, yorum yazma süresi bitince ya da müşterinin KVKK silme talebiyle takma değere çevrilir; aynı sipariş sonradan yeniden gönderilse de bu alanlar geri yazılmaz, sipariş aynı kayda işlenir. Ayrıntılar için: [Saklama süreleri](/tr/security/data-protection.md#saklama-sureleri).

Sipariş satırı alanları:

| Alan | Zorunlu | Açıklama |
|---|---|---|
| `line_reference` | Evet | Satırın sipariş içindeki kimliği (en fazla 128 karakter). Her satır için bir yorum yazılabilir. |
| `variant_key` | Evet | Ürün kodu (en fazla 128 karakter). Sayfadaki container'ın `data-review-product` değeriyle **aynı** olmalıdır. |
| `group_key` | Hayır | Ürün grubu kodu (en fazla 128 karakter). Grup düzeyinde yorum gösterimi için gereklidir. |
| `catalog_id` | Hayır | Ürünün katalogdaki kimliği (en fazla 128 karakter). |
| `title` | Hayır | Ürün adı (en fazla 300 karakter). Yorum formunda gösterilir. |
| `quantity` | Hayır | Adet; varsayılan 1. |
| `fulfilment_state` | Hayır | `placed` (varsayılan), `preparing`, `shipped`, `in_transit`, `delivered`, `cancelled`, `returned`. |
| `delivered_at` | Hayır | Teslim zamanı. `delivered` durumundaki satırlarda gönderilmesi önerilir. Daha önce iade ya da iptal edilmiş bir satır `delivered_at` olmadan yeniden `delivered` durumuyla gelirse bu durum yok sayılır; satır gerçekten yeniden teslim edildiyse teslim zamanını gönderin. |

Durum alanları yalnızca yukarıdaki değerleri kabul eder; kendi sisteminizdeki durum adlarını gönderirken bu değerlere dönüştürün.

Sipariş verisinde `delivered`, `returned` ya da `cancelled` durumuyla gelen satırlar için Sayproof bir teslimat kaydı oluşturur; bu durumda teslimat endpoint'ine ayrıca istek göndermeniz gerekmez.

İade ve iptal bilgisi yorumdan sonra da önemlidir. Siparişin ödemesi `refunded` ya da `voided` durumuna geçtiğinde ya da bir satır `returned` olarak bildirildiğinde, o siparişe ya da satıra daha önce yazılmış yorumların satın alma kanıtına "Yorumdan sonra iade edildi" işareti düşer ve otomatik kontroller yeniden çalışır. Alışveriş deneyimi davetiyle yazılmış yorumlar zaten iptal ya da iade sonrasında yazıldığı için bu işareti almaz. İptal ya da iade edilen ve henüz yorumlanmamış satırlar için alışveriş deneyimi daveti oluşturulur; bkz. [Davetler ve e-postalar](/tr/features/invitations.md#iptal-ve-iade-sonrasi-surec-daveti).

### Yanıt

```json
{ "accepted": 1, "orders": [{ "external_id": "ORD-1001", "id": "…" }] }
```

Liste **bütün olarak** işlenir: siparişlerden biri reddedilirse aynı istekteki diğer siparişler de kaydedilmez. Hata yanıtındaki kod, sorunlu alanı belirtir.

## Teslimat verisi

Sipariş verisiyle birlikte gönderilmeyen teslimat değişiklikleri için:

```json
{
  "deliveries": [
    {
      "external_id": "ORD-1001",
      "line_reference": "1",
      "state": "delivered",
      "observed_at": "2026-09-27T09:00:00+03:00",
      "source": "carrier"
    }
  ]
}
```

| Alan | Zorunlu | Açıklama |
|---|---|---|
| `external_id`, `line_reference` | Evet | Hangi siparişin hangi satırı olduğu. |
| `state` | Evet | Sipariş satırıyla aynı değerler. |
| `observed_at` | Evet | Durumun gözlendiği zaman; saat dilimiyle birlikte. |
| `source` | Hayır | `platform` (varsayılan), `carrier`, `manual`, `estimated`. |
| `confidence` | Hayır | `verified` (varsayılan) ya da `estimated`. |
| `detail` | Hayır | Kabul edilir ama saklanmaz; teslim alan kişinin adı ya da adres gibi bilgi göndermeyin. |

Tahmine dayalı bilgi (`estimated` kaynak ya da güven değeri) kanıt sayılmaz; "kargo 3 günde teslim eder" gibi bir tahmin davet ya da yorum hakkı oluşturmaz. Tahmine dayalı bir `delivered` bilgisi satırın durumunu da belirlemez: kayıtlı kalır ancak daha önce ya da sonra gelen doğrulanmış teslimat bilgisini gizlemez. Yalnızca gerçekleştiği doğrulanan teslimat bilgisini gönderin.

Tanınmayan satırlar isteğin başarısız olmasına neden olmaz; yanıttaki `unknown` listesinde döner:

```json
{ "accepted": 1, "unknown": [{ "external_id": "ORD-9", "line": "2" }] }
```

## Hatalar

| Yanıt | Anlamı |
|---|---|
| `401 refused / unknown_key` | Sunucu anahtarı tanınmıyor. Biçimi hatalı anahtarda ayrıntı `malformed_key` olur. Anahtarın tamamının `Bearer ` önekinden sonra gönderildiğini kontrol edin. |
| `401 refused / revoked_key` | Sunucu anahtarı iptal edilmiş. Yeni bir sunucu anahtarı oluşturun. |
| `403 refused / scope_missing` | Anahtarın sipariş gönderme (`orders`) izni yok. |
| `401 signature_refused / signature_mismatch` | İmzalı gönderimde imza eşleşmiyor. Body'nin imzalandıktan sonra değişip değişmediğini ve doğru şifrenin kullanıldığını kontrol edin. |
| `401 signature_refused / timestamp_outside_window` | Zaman farkı 5 dakikadan fazla. Sunucu saatini senkronize edin. |
| `401 signature_refused / timestamp_invalid` | Zaman değeri okunamadı ya da saat dilimi içermiyor. |
| `401 signature_refused / secret_unknown` | Şifre kimliği tanınmıyor ya da şifre iptal edilmiş. Özel amaçlı şifreler (bildirim adresi, e-posta teslim bildirimi) sipariş aktarımını imzalayamaz. İstekte `Authorization: Bearer` header'ı yoksa istek imzalı gönderim sayıldığı için de bu yanıt döner. |
| `400 invalid / field_missing: <alan>` | Zorunlu bir alan eksik. `field_invalid` ve `field_too_long` biçim ve uzunluk hatalarını bildirir. |
| `400 invalid / market_without_policy: <pazar>` | Bu pazar için tanımlı kural yok. |
| `400 invalid / status_unmapped: …/<değer>` | Durum değeri tanınmıyor. Yukarıdaki değerlerden birini gönderin. |
| `400 invalid / channel_unknown` | Kanal kodu yanlış ya da kanal kapalı. |
| `400 invalid / market_channel_mismatch: <pazar>` | Siparişin pazarı, siparişin bağlandığı kanalın pazarıyla aynı değil. |
| `400 invalid / batch_too_large: 100` | Tek istekte 100'den fazla sipariş gönderildi. |
| `429 rate_limited` | Sunucu anahtarı ya da şifre başına 5 dakikada 600 istek sınırı aşıldı. |

## Bağlantı denemesi

Paneldeki **Entegrasyon › Sipariş aktarımı** sekmesinin **Mağazadan gönderim** bölümündeki **Bağlantıyı dene** işlemi, mağaza sisteminizin göndereceği veriyi kaydetmeden sınar. Formda yalnızca **Ne gönderilecek?** (**Sipariş** ya da **Teslimat**) seçilir ve **Gönderilecek içerik** girilir; anahtar ya da şifre istenmez. Sonuç, verinin kabul edilip edilmediğini ve kaç kaydın kabul edileceğini gösterir; hiçbir kayıt oluşturulmaz.

Aynı kartta **Son gelen siparişler** satırı seçilen kanala gelen siparişleri sayar. **Sipariş aktarımı durumu (tüm kanallar)** bölümündeki son 7 günün istek sayıları ve **Son istekler** listesi ise bütün kanalları kapsar.
