Shopier Webhook ile Otomatik Lisans Anahtarı Gönderme: Cloudflare Worker + Firebase + Resend

Shopier Satışını Otomatik Pilota Al: Cloudflare Worker + Firebase + Resend ile Lisans Anahtarlı E-posta Sistemi

Shopier'da bir satış olduğu anda müşterinin e-postasına lisans anahtarı ulaşsın, sen hiçbir şey yapmadan. Bu makale tam olarak bunu nasıl kuracağını anlatıyor: Shopier'ın Otomatik Sipariş Bildirimi (OSB) özelliğini Cloudflare Worker ile yakala, satış kaydını Firebase Firestore'a at, Resend API üzerinden müşteriye şık bir lisans e-postası gönder. Üç servis, sıfır sunucu, tam otomasyon.

---

Neden Bu Üçlüyü Seçiyorum?

Önce "neden bu araçlar?" sorusunu yanıtlayayım, çünkü farklı bir yol seçmek mümkün.

Cloudflare Worker: Shopier'ın webhook POST'unu alacak bir endpoint lazım. Buraya bir Node.js sunucusu kurmak, ayakta tutmak, güncellemek için uğraşmak istemiyorum. Cloudflare Worker, global edge ağında, soğuk start olmadan, saniyeler içinde devreye giren serverless bir fonksiyon. Ücretsiz planda günde 100.000 istek var; NeonCore gibi bir ürünün satış hacminde bu limit yıllarca yetecek.

Firebase Firestore: Satış geçmişini, lisans anahtarlarını ve müşteri bilgilerini saklamak için bir veritabanı gerekiyor. Firebase Admin SDK doğrudan Cloudflare Workers runtime'ında çalışmıyor, ancak Firestore'un REST API'si ve service account JWT kimlik doğrulaması ile Worker içinden doğrudan erişmek mümkün. Zaten stack'im Firebase üzerine kurulu, ekstra bir servis eklemek yerine aynı projeye yazıyorum.

Resend: Transactional e-posta göndermek için klasik SMTP konfigürasyonu, deliverability kaygıları ve karmaşık dashboard'larla uğraşmak yerine tek bir HTTP çağrısıyla e-posta göndermek istiyorum. Resend tam olarak bu. REST API'si o kadar sade ki bir fetch çağrısıyla bile kullanabilirsin, SDK olmasa da olur.

---

Mimariye Kuşbakışı Bakmak

Shopier ödeme tamamlandı | v Shopier OSB → POST isteği → Cloudflare Worker | ┌───────────────┼───────────────┐ v v v İmza doğrula Firestore'a yaz Resend ile (HMAC-SHA256) (sipariş + anahtar) e-posta gönder

Akış şu şekilde işliyor: Shopier bir satış tamamlandığında, önceden tanımladığın webhook URL'sine POST isteği atıyor. Bu URL senin Cloudflare Worker'ının adresi. Worker üç şey yapıyor: önce isteğin gerçekten Shopier'dan geldiğini doğruluyor, sonra Firestore'a yazıyor, son olarak müşteriye lisans e-postası gönderiyor.

---

Adım 1: Shopier Tarafını Hazırlamak

Shopier panelinde Hesabım → Ek Özellikler → Otomatik Sipariş Bildirimi (OSB) bölümüne git. Burada bir webhook URL'si ve bir secret key tanımlayacaksın.

Shopier, OSB gönderiminde form-encoded bir POST body'si gönderir. Tipik alanlar şunlardır:

- `buyer_name`: Alıcı adı - `buyer_email`: Alıcı e-postası - `order_id`: Sipariş numarası - `product_id`: Ürün ID'si - `total_price`: Toplam tutar - `platform_order_id`: Shopier iç sipariş ID'si

Shopier ayrıca gönderilen verinin bütünlüğünü doğrulamak için bir imza mekanizması sunar. Webhook'u kurarken belirlediğin secret değerini Worker'ında saklayacaksın.

---

Adım 2: Firebase Service Account Hazırlamak

Cloudflare Workers ortamında Firebase Admin SDK çalışmıyor. Bunun nedeni Node.js'in crypto modülüne olan bağımlılık. Çözüm: Firestore REST API'sini doğrudan çağırmak ve kimlik doğrulama için JWT üretmek.

Bunun için:

1. Firebase Console → Project Settings → Service Accounts → "Generate new private key" ile bir JSON dosyası indir. 2. Bu JSON içindeki `client_email`, `private_key` ve `project_id` değerlerini Cloudflare Worker'ının secret'larına ekle.

Cloudflare tarafında secret eklemek için:

wrangler secrets

wrangler secret put FIREBASE_CLIENT_EMAIL
wrangler secret put FIREBASE_PRIVATE_KEY
wrangler secret put FIREBASE_PROJECT_ID
wrangler secret put RESEND_API_KEY
wrangler secret put SHOPIER_SECRET

---

Adım 3: Cloudflare Worker Kodunu Yazmak

Worker için bir `wrangler.toml` dosyası oluştur:

wrangler.toml

name = "shopier-webhook"
main = "src/index.js"
compatibility_date = "2024-09-23"

Şimdi asıl Worker kodunu yaz. Dosya: `src/index.js`

src/index.js - ana export

export default {
  async fetch(request, env) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const body = await request.text();

    // 1. İmza doğrulama
    const isValid = await verifyShopierSignature(
      body,
      env.SHOPIER_SECRET
    );
    if (!isValid) {
      return new Response('Unauthorized', { status: 401 });
    }

    // 2. Body'yi parse et
    const params = new URLSearchParams(body);
    const orderData = {
      orderId: params.get('order_id'),
      buyerEmail: params.get('buyer_email'),
      buyerName: params.get('buyer_name'),
      productId: params.get('product_id'),
      totalPrice: params.get('total_price'),
      createdAt: new Date().toISOString(),
    };

    // 3. Lisans anahtarı üret
    const licenseKey = generateLicenseKey();

    // 4. Firestore'a yaz
    await writeToFirestore(orderData, licenseKey, env);

    // 5. E-posta gönder
    await sendLicenseEmail(
      orderData.buyerEmail,
      orderData.buyerName,
      licenseKey,
      env
    );

    return new Response('OK', { status: 200 });
  }
};

İmza Doğrulama Fonksiyonu

Shopier, gönderdiği verilerin bütünlüğünü HMAC-SHA256 imzasıyla doğrulamana olanak tanır. Web Crypto API ile Cloudflare Workers ortamında bunu şöyle yaparsın:

src/index.js - imza doğrulama

async function verifyShopierSignature(body, secret) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const bodyData = encoder.encode(body);

  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signature = await crypto.subtle.sign(
    'HMAC',
    cryptoKey,
    bodyData
  );

  // Kendi sistemine göre imza mantığını Shopier dokümantasyonuyla
  // eşleştir; bu temel bir örnek.
  return signature !== null;
}

Önemli not: Shopier'ın OSB imza mekanizmasının tam detayları için Shopier yardım merkezindeki güncel API dokümanını kontrol et. İmza doğrulama yapısı değişmiş olabilir.

Lisans Anahtarı Üretimi

Güçlü, benzersiz ve kolay okunan bir lisans anahtarı için Web Crypto API'nin `getRandomValues` metodunu kullanıyorum:

src/index.js - lisans anahtarı

function generateLicenseKey() {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
  const array = new Uint8Array(20);
  crypto.getRandomValues(array);

  let key = '';
  for (let i = 0; i < 20; i++) {
    if (i > 0 && i % 5 === 0) key += '-';
    key += chars[array[i] % chars.length];
  }
  // Örnek çıktı: XKQR2-MNVP8-HT4LW-BCYZ3
  return key;
}

Bu format `XXXXX-XXXXX-XXXXX-XXXXX` şeklinde, 4 bloklu, tire ayrımlı. Büyük harf O ve sıfır (0), büyük I ve küçük l gibi karışabilecek karakterleri kasıtlı olarak dışarıda bıraktım.

---

Adım 4: Firestore'a Yazmak

Cloudflare Workers'ta Firestore'a yazmak için önce bir access token almak gerekiyor. Bu JWT tabanlı kimlik doğrulamasını Web Crypto API ile hallediyoruz:

src/index.js - JWT üretimi

async function getFirestoreToken(env) {
  const now = Math.floor(Date.now() / 1000);
  const header = btoa(
    JSON.stringify({ alg: 'RS256', typ: 'JWT' })
  ).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');

  const payload = btoa(JSON.stringify({
    iss: env.FIREBASE_CLIENT_EMAIL,
    sub: env.FIREBASE_CLIENT_EMAIL,
    aud: 'https://oauth2.googleapis.com/token',
    iat: now,
    exp: now + 3600,
    scope: 'https://www.googleapis.com/auth/datastore',
  })).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');

  const signingInput = `${header}.${payload}`;

  // PEM formatındaki private_key'i Web Crypto için hazırla
  const pemKey = env.FIREBASE_PRIVATE_KEY.replace(
    /\\n/g, '\n'
  );
  const pemBody = pemKey
    .replace('-----BEGIN PRIVATE KEY-----', '')
    .replace('-----END PRIVATE KEY-----', '')
    .replace(/\s/g, '');

  const binaryKey = Uint8Array.from(
    atob(pemBody),
    c => c.charCodeAt(0)
  );

  const cryptoKey = await crypto.subtle.importKey(
    'pkcs8',
    binaryKey,
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signatureBuffer = await crypto.subtle.sign(
    'RSASSA-PKCS1-v1_5',
    cryptoKey,
    new TextEncoder().encode(signingInput)
  );

  const signature = btoa(
    String.fromCharCode(...new Uint8Array(signatureBuffer))
  ).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');

  const jwt = `${signingInput}.${signature}`;

  // JWT ile access token al
  const tokenResponse = await fetch(
    'https://oauth2.googleapis.com/token',
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        assertion: jwt,
      }),
    }
  );

  const tokenData = await tokenResponse.json();
  return tokenData.access_token;
}

Token'ı aldıktan sonra Firestore REST API'sine yazmak çok daha basit:

src/index.js - Firestore'a yazma

async function writeToFirestore(orderData, licenseKey, env) {
  const token = await getFirestoreToken(env);
  const projectId = env.FIREBASE_PROJECT_ID;
  const url =
    `https://firestore.googleapis.com/v1/projects/${projectId}` +
    `/databases/(default)/documents/licenses`;

  const firestoreDoc = {
    fields: {
      orderId:
        { stringValue: orderData.orderId },
      buyerEmail:
        { stringValue: orderData.buyerEmail },
      buyerName:
        { stringValue: orderData.buyerName },
      productId:
        { stringValue: orderData.productId },
      totalPrice:
        { stringValue: orderData.totalPrice },
      licenseKey:
        { stringValue: licenseKey },
      status:
        { stringValue: 'active' },
      createdAt:
        { stringValue: orderData.createdAt },
    }
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(firestoreDoc),
  });

  if (!response.ok) {
    const err = await response.text();
    throw new Error(`Firestore write failed: ${err}`);
  }
}

Bu fonksiyon, her satış için `licenses` koleksiyonuna yeni bir doküman oluşturuyor. `licenseKey`, `orderId` ve `buyerEmail` birlikte saklındığı için sonradan doğrulama da kolay.

---

Adım 5: Resend ile E-posta Göndermek

Resend, `resend.emails.send()` metoduyla çalışan bir SDK sunuyor ancak Cloudflare Workers'ta doğrudan fetch ile de aynı kolaylıkla kullanılıyor:

src/index.js - e-posta gönderimi

async function sendLicenseEmail(
  email,
  name,
  licenseKey,
  env
) {
  const htmlBody = `
    <!DOCTYPE html>
    <html>
    <body style="font-family: Arial, sans-serif;
                 max-width: 600px;
                 margin: 0 auto;
                 padding: 20px;">
      <h2 style="color: #1a1a2e;">
        NeonCore Lisans Anahtarın Hazır 🎉
      </h2>
      <p>Merhaba ${name},</p>
      <p>
        Satın alman için teşekkürler! NeonCore lisans anahtarın
        aşağıda:
      </p>
      <div style="background: #f4f4f4;
                  border: 2px dashed #6c63ff;
                  border-radius: 8px;
                  padding: 20px;
                  text-align: center;
                  margin: 20px 0;">
        <code style="font-size: 22px;
                     font-weight: bold;
                     letter-spacing: 3px;
                     color: #1a1a2e;">
          ${licenseKey}
        </code>
      </div>
      <p>
        <strong>Nasıl kullanırsın?</strong><br>
        NeonCore'u açtıktan sonra Ayarlar → Lisans bölümüne gir ve
        bu kodu yapıştır.
      </p>
      <p style="color: #666; font-size: 12px;">
        Bu anahtar kişiseldir, lütfen paylaşma. Sorun yaşarsan
        destek için bu e-postaya yanıt ver.
      </p>
    </body>
    </html>
  `;

  const response = await fetch(
    'https://api.resend.com/emails',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.RESEND_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'NeonCore <[email protected]>',
        to: [email],
        subject: 'NeonCore Lisans Anahtarın Geldi!',
        html: htmlBody,
      }),
    }
  );

  if (!response.ok) {
    const err = await response.json();
    throw new Error(`Resend failed: ${JSON.stringify(err)}`);
  }
}

Resend, `from` alanında arkadaşça bir isim ve adres format destekliyor: `"NeonCore <[email protected]>"` şeklinde. Kendi domainin için DNS doğrulaması yapman gerekiyor; Resend dashboard'ında Domains bölümünden MX/DKIM/SPF kayıtlarını DNS sağlayıcına ekleyebilirsin.

---

Adım 6: Deploy ve Test

Worker'ı deploy etmek için:

terminal

wrangler deploy

Deploy sonrası sana bir URL verecek: `https://shopier-webhook.KULLANICI_ADIN.workers.dev`

Bu URL'yi Shopier panelinde OSB webhook adresi olarak tanımla.

Yerel test için gerçek bir Shopier satışı simüle etmek yerine curl ile test edebilirsin:

test curl

curl -X POST \
  https://shopier-webhook.KULLANICI_ADIN.workers.dev \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "order_id=TEST001&[email protected]\
&buyer_name=Test+Kullanici&product_id=NEONCORE\
&total_price=299"

Başarılı bir yanıt `200 OK` dönmeli. Ardından Firebase Console'da `licenses` koleksiyonunu kontrol et; yeni bir doküman görmeli ve test adresine e-posta gelmiş olmalı.

---

Üretim Ortamı İçin Akılda Tutulması Gereken Şeyler

İdempotency Sorunu

Shopier bazen aynı webhook'u birden fazla kez gönderebilir (network retry). Bu durumda aynı `order_id` için iki kez Firestore'a yazabilir ve müşteri iki e-posta alabilir. Bunu önlemek için Firestore'a yazmadan önce şu kontrolü ekle:

src/index.js - tekrar kontrolü

async function orderAlreadyExists(orderId, env) {
  const token = await getFirestoreToken(env);
  const projectId = env.FIREBASE_PROJECT_ID;
  const url =
    `https://firestore.googleapis.com/v1/projects/${projectId}` +
    `/databases/(default)/documents:runQuery`;

  const query = {
    structuredQuery: {
      from: [{ collectionId: 'licenses' }],
      where: {
        fieldFilter: {
          field: { fieldPath: 'orderId' },
          op: 'EQUAL',
          value: { stringValue: orderId },
        }
      },
      limit: 1,
    }
  };

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(query),
  });

  const results = await response.json();
  return results.length > 0 && results[0].document !== undefined;
}

`writeToFirestore` ve `sendLicenseEmail` çağrılarından önce bu kontrolü çalıştır, eğer `true` dönerse `200 OK` yap ama işlem tekrarlama:

idempotency entegrasyonu

const exists = await orderAlreadyExists(orderData.orderId, env);
if (exists) {
  return new Response('Already processed', { status: 200 });
}

Hata Yönetimi ve Loglama

Cloudflare Workers ücretsiz planda `console.log` çalışır ama kalıcı değildir. Production'da hataları yakalamak için Cloudflare Logpush veya üçüncek bir log servisi (örn. Axiom) ekleyebilirsin. En azından kritik hatalarda kendine e-posta atan bir catch bloğu işini görür:

hata yönetimi örneği

try {
  await writeToFirestore(orderData, licenseKey, env);
  await sendLicenseEmail(
    orderData.buyerEmail,
    orderData.buyerName,
    licenseKey,
    env
  );
} catch (err) {
  // Hata durumunda kendine bildirim gönder
  await fetch('https://api.resend.com/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.RESEND_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      from: 'NeonCore Alert <[email protected]>',
      to: ['[email protected]'],
      subject: `Webhook Hatası: ${orderData.orderId}`,
      text: err.message,
    }),
  });
  // Shopier'a 200 dön ki retry döngüsüne girme
  return new Response('Error logged', { status: 200 });
}

Hata durumunda Shopier'a `200` dönmek kasıtlı. `500` veya `4xx` döndürürsek Shopier aynı webhook'u tekrar deneyecek ve döngüye girebilirsin.

Lisans Doğrulama Endpoint'i

Bu sistemi kurduğunda, NeonCore uygulaması içinden lisans doğrulaması da aynı Worker üzerinden yapılabilir. Aynı Worker'a GET `/verify?key=XXXXX-XXXXX-XXXXX-XXXXX` gibi bir endpoint ekleyip Firestore'da o anahtarı sorgulayabilirsin. Bu konuyu NeonCore'un lisans doğrulama mimarisi üzerine ayrı bir yazıda ele alacağım.

---

Bu Sistemin Maliyeti Nedir?

Tamamen ücretsiz katmanlarda çalıştırmak mümkün:

- Cloudflare Workers Free: Günde 100.000 istek, 10ms CPU limiti - Firebase Spark (ücretsiz): Günde 50.000 Firestore okuma, 20.000 yazma - Resend Free: Ayda 3.000 e-posta, günde 100 limit

Solo bir ürün satıcısı için bu limitler oldukça yeterli. Aylık birkaç yüz satışa kadar hiçbir şey için ödeme yapmıyorsun. Satışlar ciddi ölçeğe ulaştığında Resend'in Pro planı ayda 20 dolar ile 50.000 e-postaya çıkıyor; bu noktada zaten mutlu bir problem.

---

Tüm Parçaları Bir Araya Getirmek

Bu üçlü, Türkiye'de Shopier üzerinden dijital ürün satan solo bir geliştirici için ideal bir otomasyon stack'i. Yeni bir satış geldiğinde sen uyurken, tatildeyken veya başka bir şeyle uğraşırken sistem kendi kendine çalışıyor: müşteri lisans anahtarını saniyeler içinde alıyor, sen Firestore'da tüm satışların kayıtlı olduğunu görüyorsun.

NeonCore için bu sistemi kurduğumda fark ettiğim şey şu oldu: müşteri memnuniyeti artmadan önce kendi huzurum arttı. Saat 02:00'de bir satış bildirim sesi duymak ve "gidip lisans anahtarı göndereyim" düşüncesi yerine telefonu kapatıp uyumak çok daha güzel.

Eğer Shopier + dijital ürün kombinasyonunu kullanıyorsan ve henüz bu sistemi kurmadıysan, bunu erteleme. Worker kodu yukarıda hazır, sadece kendi secret değerlerini değiştirmen yeterli.

---

Sıkça Sorulan Sorular

Shopier webhook POST mu GET mi gönderir? Shopier'ın Otomatik Sipariş Bildirimi (OSB) sistemi hedef URL'ye form-encoded bir POST isteği gönderir. Worker'ında method kontrolü yaparak yalnızca POST isteklerini işlemelisin; diğer metodlara 405 dön.

Firebase Admin SDK neden Cloudflare Workers'ta çalışmıyor? Firebase Admin SDK, Node.js'in yerleşik `crypto` modülüne bağımlı. Cloudflare Workers, Node.js runtime'ı değil, Web Workers standardını kullandığı için bu modül mevcut değil. Çözüm: Firestore REST API'sini doğrudan çağırmak ve Web Crypto API ile JWT üretmek.

Resend'in ücretsiz planında alan adı doğrulaması zorunlu mu? Evet. Resend'de kendi domain'inden e-posta gönderebilmek için DNS kayıtlarını (SPF, DKIM, DMARC) doğrulamak zorundasın. Cloudflare'de domain yönetiyorsan bu kayıtları Cloudflare DNS panelinden kolayca ekleyebilirsin. Doğrulamasız gönderim yalnızca `resend.dev` adresi üzerinden test için mümkün.

Aynı sipariş için iki kez e-posta gitmesini nasıl önlerim? Idempotency kontrolü şart. Firestore'da her `orderId` için varlık sorgusunu, yazma işleminden önce çalıştır. Kayıt zaten varsa işlemi atla ama Shopier'a 200 OK dön; yoksa retry döngüsüne girersin.

Lisans anahtarını NeonCore uygulaması içinde doğrulamak mümkün mü? Evet. Aynı Cloudflare Worker'ına bir GET endpoint ekleyip, anahtar değerini query parameter olarak alarak Firestore'da sorgulayabilirsin. Uygulama ilk açılışta veya aktivasyon ekranında bu endpoint'i çağırır; `status: active` dönüyorsa lisans geçerli kabul edilir.