# Webhook'lar

> Webhook aboneliği, olay kataloğu ve gereken kapsamlar, yaşam döngüsü olayları, teslimat biçimi, yeniden deneme ve idempotency.
>
> Kaynak: https://tecof-app-dev.vercel.app/app-webhooks · Bölüm: Uygulama geliştirme · Son güncelleme: 2018-10-20

Webhook'lar, mağazada bir olay olduğunda (sipariş ödendi, ürün güncellendi, müşteri oluşturuldu…) Tecof'un uygulamanıza imzalı bir `POST` isteği göndermesidir. Platform uygulamaları ayrıca kurulum, kaldırma, kapsam, abonelik ve ayar değişikliklerini bildiren **yaşam döngüsü olaylarını** abonelik gerekmeden alır.

---

## Abonelik

Abonelik `tecof.app.json` içinde tanımlanır:

```json
{
  "scopes": ["orders:read", "customers:read"],
  "webhooks": {
    "path": "/api/webhooks",
    "events": ["order.paid", "order.cancelled", "order.refunded", "customer.created"]
  }
}
```

- `path`, `appUrl`'e göredir ve `/` ile başlar. Teslimat adresi: `appUrl + webhooks.path`. Test mağazası kurulumlarında, `tecof app dev` açıksa `appUrl` yerine tünel adresi (`devUrl`) kullanılır; adres teslimat anında çözülür.
- **`webhooks.path` tanımlı değilse** yaşam döngüsü olayları (`app.installed`, `app.uninstalled`…) ve uygulama olayları da teslim edilemez. Mağaza olayı dinlemeseniz bile `path` tanımlayın.
- Her olayın gerektirdiği okuma kapsamı `scopes` içinde olmalıdır; aksi hâlde yapılandırma reddedilir.
- `*` (tüm olaylar) jokeri uygulamalara **kapalıdır**.
- Mağaza uygulamayı kurduğunda abonelik otomatik açılır. Yapılandırma değişince abonelik eşitlenir: etkin olaylar, yapılandırmadaki olaylarla mağazanın **verdiği** kapsamların kesişimidir.

---

## Olay Kataloğu ve Gereken Kapsam

| Olay | Açıklama | Gereken kapsam |
| :--- | :--- | :--- |
| `order.created` | Sipariş oluşturuldu | `orders:read` |
| `order.paid` | Sipariş ödendi | `orders:read` |
| `order.updated` | Sipariş durumu güncellendi | `orders:read` |
| `order.fulfilled` | Sipariş kargolandı | `orders:read` |
| `order.cancelled` | Sipariş iptal edildi | `orders:read` |
| `order.refunded` | Sipariş iade edildi | `orders:read` |
| `order.return_requested` | İade talebi oluşturuldu | `orders:read` |
| `order.return_approved` | İade talebi onaylandı | `orders:read` |
| `order.return_rejected` | İade talebi reddedildi | `orders:read` |
| `order.return_received` | İade ürünü teslim alındı | `orders:read` |
| `order.return_resolved` | İade talebi sonuçlandı | `orders:read` |
| `order.shipping_updated` | Kargo bilgisi güncellendi | `orders:read` |
| `product.created` | Ürün oluşturuldu | `products:read` |
| `product.updated` | Ürün güncellendi | `products:read` |
| `product.deleted` | Ürün silindi | `products:read` |
| `inventory.low_stock` | Düşük stok uyarısı | `products:read` |
| `inventory.out_of_stock` | Stok tükendi | `products:read` |
| `review.created` | Yorum gönderildi | `products:read` |
| `review.approved` | Yorum onaylandı | `products:read` |
| `customer.created` | Müşteri oluşturuldu | `customers:read` |
| `customer.updated` | Müşteri güncellendi | `customers:read` |
| `customer.deleted` | Müşteri silindi | `customers:read` |
| `subscriber.created` | Bülten abonesi eklendi | `marketing:read` |
| `cms.item.published` | CMS içeriği yayınlandı | `cms:read` |
| `form.submitted` | Form gönderildi | `cms:read` |

Başka uygulamaların yayınladığı `app.<slug>.<ad>` olayları da aynı biçimde teslim edilir; bkz. [Uygulamalar Arası İletişim](/app-interop).

---

## Yaşam Döngüsü Olayları

Bu olaylar **her zaman** gönderilir; `webhooks.events` içinde listelenmeleri gerekmez.

| Olay | Ne zaman | `data` |
| :--- | :--- | :--- |
| `app.installed` | Mağaza uygulamayı kurduğunda | `{ installationId, merchantId, scopes, planKey, subscription, isTestInstall }` |
| `app.uninstalled` | Mağaza uygulamayı kaldırdığında. **Tek deneme**, 8 sn zaman aşımı | `{ installationId, merchantId }` |
| `app.scopes_updated` | Mağaza ek izin isteğini onayladığında | `{ scopes }` |
| `app.subscription.updated` | Abonelik durumu her değiştiğinde | `{ planKey, status, trialEndsAt, currentPeriodEnd, chargeId }` |
| `app.settings.updated` | Mağaza paneldeki ayar formunu kaydettiğinde | `{ keys: [...], secretKeys: [...] }` — **değer içermez** |

**`app.uninstalled`:** Kurulumun token'ları bu teslimat **bittikten sonra** iptal edilir; işleyiciniz 8 saniye içinde Tecof API'sini hâlâ çağırabilir (ör. son durumu okumak için). Teslimattan sonra kurulum kaldırılır, kurulumun ayarları ve sırları silinir. Olay yeniden denenmez; kaçırırsanız temizliği bir sonraki API çağrısında aldığınız `401` ile tetikleyin.

**`app.settings.updated`** yalnız mağaza paneldeki ayar formunu kaydettiğinde gelir; uygulamanızın `PUT /api/v1/app/settings` ile yaptığı yazmalar bu olayı **üretmez**. Olay değer taşımaz: değişen ayarları `api.settings.get()` ve `api.secrets.get(key)` ile okuyun.

---

## Teslimat Biçimi

```http
POST /api/webhooks HTTP/1.1
Host: isbasi.apps.tecof.site
Content-Type: application/json
User-Agent: Tecof-Webhooks/1.0
X-Tecof-Event: order.paid
X-Tecof-Event-Id: evt_7f3a2c9e1b
X-Tecof-Delivery-Attempt: 1
X-Tecof-Merchant-Id: 66f0c0ffee0000000000abcd
X-Tecof-Installation-Id: 6720aa00bb11cc22dd33ee55
X-Tecof-App-Id: tcfa_xxxxxxxxxxxxxxxx
X-Tecof-Timestamp: 1760087770
X-Tecof-Signature: t=1760087770,v1=5f2c…e91a
X-Webhook-Event: order.paid
```

```json
{
  "id": "evt_7f3a2c9e1b",
  "event": "order.paid",
  "test": false,
  "timestamp": "2026-10-10T09:16:10.000Z",
  "merchantId": "66f0c0ffee0000000000abcd",
  "installationId": "6720aa00bb11cc22dd33ee55",
  "data": {
    "orderId": "6720bb00cc11dd22ee33ff44",
    "orderNumber": "TCF-1001",
    "status": "confirmed",
    "paymentStatus": "paid",
    "totalPrice": 1050,
    "taxPrice": 175,
    "pricesIncludeTax": true,
    "currencyCode": "TRY",
    "customerEmail": "musteri@example.com",
    "merchantId": "66f0c0ffee0000000000abcd",
    "merchantName": "Örnek Mağaza"
  }
}
```

| Başlık | Açıklama |
| :--- | :--- |
| `X-Tecof-Event` | Olay adı |
| `X-Tecof-Event-Id` | Olay kimliği; yeniden denemelerde **aynı** kalır — idempotency anahtarı |
| `X-Tecof-Delivery-Attempt` | Deneme sayısı (1'den başlar) |
| `X-Tecof-Merchant-Id` | Mağaza kimliği |
| `X-Tecof-Installation-Id` | Kurulum kimliği (kurulum token'ı almak için) |
| `X-Tecof-App-Id` | Uygulamanın istemci kimliği |
| `X-Tecof-Timestamp`, `X-Tecof-Signature` | İmza; bkz. [İmzalı İstekler](/app-auth) |
| `X-Webhook-Event` | Geriye uyum için olay adı |

Mağaza olaylarının `data` alanı, mağaza webhook'larının gönderdiği yükün **aynısıdır**; Tecof `merchantId` ve `merchantName` alanlarını ekler. Sipariş olaylarında `orderId`, `orderNumber`, `status`, `paymentStatus`, `totalPrice`, `taxPrice`, `pricesIncludeTax`, `currencyCode`, `customerEmail` gibi özet alanlar gelir. Satırlar, adresler ve vergi kırılımı dahil siparişin tam ve kararlı şekli için `GET /api/v1/orders/:id` ile okuyun (`api.orders.get(data.orderId)`). Bkz. [API ile Veri Erişimi](/app-api).

Platform uygulamalarına ham sır içeren `X-Webhook-Secret` başlığı **gönderilmez** (yalnız eski birinci parti ve mağaza webhook'larında sürer). Doğrulama yalnız `X-Tecof-Signature` ile yapılır.

---

## Yeniden Deneme ve Otomatik Pasifleştirme

| Kural | Davranış |
| :--- | :--- |
| Başarı | Herhangi bir `2xx` yanıtı teslim edildi sayılır |
| Zaman aşımı | Deneme başına **8 saniye** |
| Başarısızlık | `2xx` dışı yanıt, bağlantı hatası ya da zaman aşımında yeniden denenir. Toplam **3 deneme**: ilk teslimat + 2 yeniden deneme (+2 dk, +15 dk) |
| Gövde | Yeniden denemeler ve elle yeniden gönderim **tam gövdeyle** (≤ 256 KB) yapılır; `X-Tecof-Event-Id` değişmez, `X-Tecof-Delivery-Attempt` artar |
| Otomatik pasifleştirme | **20 ardışık** başarısız teslimattan sonra abonelik pasife alınır |
| Yeniden etkinleştirme | `tecof app config push` (yapılandırma yazımı) uygulamanın tüm pasif aboneliklerini yeniden etkinleştirir; mağaza da paneldeki Webhook ekranından aboneliği açabilir |
| `app.uninstalled` | Tek deneme (8 sn); yeniden denenmez |

Teslimat geçmişinizi geliştirici API'sinden izleyebilirsiniz: `GET /api/developer/apps/:id/deliveries`.

---

## Idempotency

Aynı olay birden fazla kez gelebilir (yeniden deneme, ağ kopması). İşleyicinizi idempotent yazın:

1. **Olay kimliği:** `X-Tecof-Event-Id` (gövdede `id`) yeniden denemelerde aynıdır. İşlenen kimlikleri kendi deponuzda tutup tekrarı atlayın.
2. **Kaynak durumu:** Daha sağlam yol, işin yapılıp yapılmadığını kaynağın kendisinden anlamaktır. Örneğin fatura kesen bir uygulama faturayı oluşturunca siparişe `invoiceNumber` metafield'ı yazar; aynı sipariş için gelen ikinci `order.paid` bu metafield'ı görüp atlanır.
3. **Sıra:** Yeniden denemeler nedeniyle olaylar oluş sırasından farklı sırada gelebilir. Kararı olay yüküne değil, API'den okuduğunuz güncel duruma göre verin.

---

## Örnek İşleyici (Next.js App Router)

SDK'nın `createWebhookHandler` yardımcısı doğrulamayı, olay yönlendirmeyi ve yanıt kodlarını üstlenir. Dönen fonksiyon doğrudan bir Route Handler'dır:

```ts
// app/api/webhooks/route.ts
import { createWebhookHandler } from "@tecof/app-sdk";
import { tecof } from "@/lib/tecof";

export const POST = createWebhookHandler({
  app: tecof,
  handlers: {
    "app.installed": async (event) => {
      await tecof.client(event.installationId).settings.set({ invoiceMode: "draft" }, { merge: true });
    },

    "app.uninstalled": async (event) => {
      // Token'lar bu teslimat bittikten sonra iptal edilir; gerekirse burada API'yi çağırabilirsiniz.
      // Kendi sistemlerinizdeki kopyaları temizleyin (8 sn içinde).
    },

    "app.settings.updated": async (event) => {
      // Değerler gelmez; gerekirse api.settings.get() / api.secrets.get(key) ile okuyun
    },

    "order.paid": async (event) => {
      const api = tecof.client(event.installationId);
      const orderId = String(event.data.orderId);

      const existing = await api.metafields.list({ ownerType: "order", ownerId: orderId, namespace: "isbasi" });
      if (existing.some((m: { key: string }) => m.key === "invoiceNumber")) return; // zaten işlendi

      const order = await api.orders.get(orderId);
      // … faturayı oluştur, sonra siparişe invoiceNumber metafield'ı yaz
    },
  },
});
```

| Durum | `createWebhookHandler` yanıtı |
| :--- | :--- |
| İmza ya da zaman damgası geçersiz | `401` |
| Test olayı (`test: true`) | `200` (varsayılan olarak işleyiciler çalıştırılmaz) |
| İşleyicisi tanımlı olmayan olay | `200` |
| İşleyici başarıyla döndü | `200` |
| İşleyici hata fırlattı | `2xx` olmayan yanıt → Tecof yeniden dener |

### SDK yardımcısı olmadan

```ts
// app/api/webhooks/route.ts
import { tecof } from "@/lib/tecof";

export async function POST(req: Request) {
  const rawBody = await req.text(); // ham gövde — imza bunun üzerinden hesaplanır

  let event;
  try {
    event = tecof.verifyWebhook(rawBody, req.headers); // Headers ya da düz nesne kabul eder
  } catch {
    return new Response("invalid signature", { status: 401 });
  }

  if (event.test) return new Response(null, { status: 204 });

  switch (event.event) {
    case "order.paid":
      // …
      break;
  }
  return new Response(null, { status: 204 });
}
```

İpuçları:

- İşleyiciyi 8 saniyelik zaman aşımının altında tutun ve işlem bitince `2xx` dönün. `2xx` dışındaki her yanıt yeniden denemeye ve ardışık hata sayacına yazılır.
- Kalıcı olarak işlenemeyecek bir olay için (ör. mağaza ayarları eksik) `2xx` dönüp durumu kendi tarafınızda kaydedin; aksi hâlde yeniden denemeler ve ardışık hatalar aboneliği pasife düşürebilir.

---

## Test Etme

| Yöntem | Nasıl |
| :--- | :--- |
| Test mağazası + tünel | `tecof app dev` çalışırken test mağazasında gerçek olay üretin (ör. sipariş oluşturup ödendi işaretleyin); webhook doğrudan yerel sunucunuza gelir |
| Panelden test teslimatı | Mağaza panelindeki Webhook ekranında aboneliği seçip **"Test Et"** ile `test: true` taşıyan bir teslimat gönderilebilir |
| Yerel imzalı istek | Aşağıdaki `curl` + `openssl` ile kendi uçunuza imzalı gövde gönderin |
| Teslimat günlüğü | `GET /api/developer/apps/:id/deliveries` ile yanıt kodlarını ve denemeleri izleyin |

```bash
SECRET="$TECOF_CLIENT_SECRET"
T=$(date +%s)
BODY='{"id":"evt_local_1","event":"order.paid","test":false,"timestamp":"2026-10-10T09:16:10.000Z","merchantId":"66f0c0ffee0000000000abcd","installationId":"6720aa00bb11cc22dd33ee55","data":{"orderId":"6720bb00cc11dd22ee33ff44","orderNumber":"TCF-1001"}}'
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')

curl -X POST http://localhost:3000/api/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Tecof-Event: order.paid" \
  -H "X-Tecof-Event-Id: evt_local_1" \
  -H "X-Tecof-Delivery-Attempt: 1" \
  -H "X-Tecof-Installation-Id: 6720aa00bb11cc22dd33ee55" \
  -H "X-Tecof-Timestamp: $T" \
  -H "X-Tecof-Signature: t=$T,v1=$SIG" \
  --data-binary "$BODY"
```

Elle imzalı istekte `installationId` gerçek bir test kurulumuna ait değilse API çağrıları (`api.orders.get`) başarısız olur; imza ve yönlendirme mantığını bu yolla, uçtan uca akışı test mağazasında tünelle doğrulayın.

---

## Mağaza webhook’ları (panelden)

Uygulama yazmadan, bir mağazanın olaylarını kendi sunucunuza göndermek için mağaza sahibi panelde (**E-ticaret → Ayarlar → Webhook’lar**) webhook tanımlayabilir. Bu webhook’lar aynı olay kataloğunu kullanır, `*` (tüm olaylar) jokerine izin verir ve eski usul `X-Webhook-Secret` başlığıyla doğrulanır. Panel aşağıdaki uçları JWT ile çağırır; üçüncü taraf entegrasyonlar için platform uygulaması önerilir.

| Metot | Uç | Açıklama |
| :--- | :--- | :--- |
| `GET` | `/api/merchant/ecommerce/webhooks` | Liste |
| `POST` | `/api/merchant/ecommerce/webhooks` | Oluştur |
| `PUT` · `DELETE` | `/api/merchant/ecommerce/webhooks/:id` | Güncelle · sil |
| `POST` | `/api/merchant/ecommerce/webhooks/:id/test` | Test gönderimi |
| `GET` | `/api/merchant/ecommerce/webhooks/deliveries` | Teslimat geçmişi |
| `POST` | `/api/merchant/ecommerce/webhooks/deliveries/:id/redeliver` | Aynı yükle yeniden gönder |
