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.