Webhook'lar
Pushizo, app'inizdeki olayları (teslimat, açılma, abonelik, kullanıcı, olay, dönüşüm, kampanya) HTTPS üzerinden sizin sunucunuza imzalı olarak iletir. Endpoint'ler dashboard'da Geliştirici → Webhooks ekranından ya da…
Pushizo, app'inizdeki olayları (teslimat, açılma, abonelik, kullanıcı, olay, dönüşüm, kampanya) HTTPS üzerinden sizin sunucunuza imzalı olarak iletir. Endpoint'ler dashboard'da Geliştirici → Webhooks ekranından ya da Public API'den (/v1/apps/{app_id}/webhooks, admin kapsamı) yönetilir.
Gövdeyi Liquid şablonuyla dönüştürmek, olayları koşullarla (ör. yalnızca belirli kampanya) daraltmak ve sonucu önizlemek için bkz. Event Streams. delivery.* olayları data.campaign_id alanını da taşır (kampanya dışı mesajlarda null).
Olay türleri
| Grup | Olaylar |
|---|---|
| Mesaj | message.queued, message.completed, message.failed |
| Teslimat | delivery.sent, delivery.delivered, delivery.failed, delivery.opened, delivery.clicked, delivery.bounced, delivery.complained, delivery.unsubscribed |
| In-app | in_app.impression, in_app.clicked (button_id), in_app.dismissed |
| Abonelik | subscription.created, subscription.unsubscribed, subscription.invalidated |
| Kullanıcı | user.created, user.updated, user.merged, user.deleted |
| Olay | event.received (olay adı filtresi zorunlu: event_filters.event_names, en az bir ad) |
| Dönüşüm | conversion.created |
| Kampanya | campaign.scheduled, campaign.started, campaign.completed |
| Journey | journey.entered, journey.step_completed, journey.completed, journey.exited |
E-posta/SMS geri bildirimleri (Faz 10, bkz. messaging.md) aynı olaylarla iletilir: sağlayıcı teslimi delivery.delivered, açılma pikseli/tıklama yönlendirmesi ve sağlayıcı açılma/tıklama olayları delivery.opened / delivery.clicked (url alanıyla), kalıcı bounce delivery.failed (error_code: "bounced", geriye dönük uyum) ve delivery.bounced (bounce_type: "hard", error_code, error_message) + subscription.invalidated; şikâyet delivery.complained, abonelikten çıkma bağlantısı delivery.unsubscribed (her ikisi teslimat başına bir kez) + subscription.unsubscribed; SMS STOP subscription.unsubscribed.
In-app olayları SDK'nın gösterim/tıklama/kapatma bildiriminden üretilir: data = { in_app_message_id, name, subscription_id, user: { id, external_id }, button_id? }. delivery.* ve in_app.* olayları batch modunda gruplanabilir.
Journey olayları (Faz 11, bkz. journeys.md) şu ortak data alanlarını taşır: { journey_id, run_id, version, user: { id, external_id } }. Olaya göre ek alanlar:
journey.entered:entry(giriş bağlamı).journey.step_completed:node_id,node_type,status(completed/skipped/failed),branch,result. Batch modunda gruplanabilir.journey.completedvejourney.exited:reason(ör.manual,journey_archived, çıkış kuralı),status,goal_reached,goal_id.
Toplu içe aktarma (CSV import) ile oluşan kullanıcı/abonelikler webhook üretmez.
Gövde
{
"id": "whe_01J…",
"type": "delivery.clicked",
"created_at": "2026-10-07T12:00:00.000Z",
"app_id": "app_01J…",
"data": {
"delivery_id": "dlv_…",
"message_id": "msg_…",
"subscription_id": "sub_…",
"channel": "ios_push",
"user": { "id": "usr_…", "external_id": "u1" },
"button_id": "track"
}
}Tek olay gövdesi 64 KB'ı aşarsa data yerine { "truncated": true, "reason": "payload_too_large", "subject_id": … } gönderilir; ayrıntıyı API'den okuyun.
Batch modu (endpoint ayarı batch: true): yüksek hacimli delivery.* olayları 100'erli tek istekte { "events": [ … ] } gövdesiyle gönderilir; diğer olaylar tekil kalır.
Başlıklar
| Başlık | Açıklama |
|---|---|
Pushizo-Signature | t=<unix saniye>,v1=<hex>; rotasyon penceresinde iki v1 |
Pushizo-Event-Id | Olay kimliği (whe_…); aynı olay tekrar gönderilirse (retry/replay) değişmez — tekrarları bununla ayıklayın |
Pushizo-Event-Type | Olay türü; batch gövdede batch |
Pushizo-Batch-Id | Yalnızca batch gövdede |
Pushizo-Delivery-Attempt | Deneme sayısı (1–7) |
Endpoint'e eklediğiniz özel başlıklar (ör. Authorization) da gönderilir; değerleri şifreli saklanır ve API'de yalnızca adları görünür.
İmza doğrulama
v1 = HMAC-SHA256(secret, t + "." + ham_gövde) (hex). Alıcı:
- Ham gövdeyi kullanmalıdır (JSON'u ayrıştırıp yeniden serileştirmeyin).
tdeğerinin şimdiden en fazla 5 dakika farklı olduğunu doğrulamalıdır — eskitile tekrar gönderilen (replay) istekler reddedilir.- İmzayı sabit zamanlı karşılaştırmayla doğrulamalıdır; başlıktaki
v1imzalarından biri eşleşirse geçerlidir. Pushizo-Event-Idile tekrarları ayıklamalı ve hızlıca 2xx dönmelidir (ağır işi kuyruğa alın; zaman aşımı 10 sn).
Node.js (@pushizo/node)
import express from 'express';
import { constructWebhookEvents, WebhookSignatureError } from '@pushizo/node';
const app = express();
app.post('/hooks/pushizo', express.raw({ type: 'application/json' }), (req, res) => {
try {
const events = constructWebhookEvents({
payload: req.body, // Buffer (ham gövde)
header: req.headers['pushizo-signature'],
secret: process.env.HS_WEBHOOK_SECRET!,
});
for (const event of events) enqueue(event); // Pushizo-Event-Id / event.id ile tekrarları ayıklayın
res.sendStatus(204);
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send(err.reason);
throw err;
}
});SDK kullanmadan:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody: Buffer, header: string, secret: string, toleranceSec = 300): boolean {
const pairs = header.split(',').map((p) => p.split('=') as [string, string]);
const t = Number(pairs.find(([k]) => k === 't')?.[1]);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
return pairs
.filter(([k]) => k === 'v1')
.some(([, v]) => {
const sig = Buffer.from(v, 'hex');
return sig.length === expected.length && timingSafeEqual(sig, expected);
});
}PHP (pushizo/pushizo-php)
$payload = file_get_contents('php://input'); // ham gövde
$header = $_SERVER['HTTP_PUSHIZO_SIGNATURE'] ?? null;
try {
$events = Pushizo\Webhook::constructEvents($payload, $header, getenv('HS_WEBHOOK_SECRET'));
} catch (Pushizo\WebhookSignatureException $e) {
http_response_code(400);
exit($e->reason);
}
foreach ($events as $event) {
// $event['id'] ile tekrarları ayıklayın
}
http_response_code(204);SDK kullanmadan: hash_hmac('sha256', $t . '.' . $payload, $secret) ve hash_equals(); abs(time() - $t) <= 300.
Doğrulama algoritması @pushizo/node (sdk/node/src/__tests__), PHP SDK (sdk/php/tests/run.php) ve sunucu (packages/webhooks) testlerinde aynı vektörlerle sınanır; eski t ile replay reddi testlerdedir.
Secret yönetimi
- Secret (
whsec_…) yalnızca endpoint oluşturulurken ve rotasyonda bir kez gösterilir; dashboard'da "Göster" ile tekrar görüntülenebilir (audit kaydı oluşur). Sunucuda envelope encryption ile şifreli saklanır. - Rotasyon (
POST …/webhooks/{id}/rotate-secret): yeni secret hemen kullanılır; eski secret 24 saat boyunca ikinciv1imzası olarak gönderilmeye devam eder. Alıcınızı bu süre içinde yeni secret'a geçirin.
Yeniden deneme, dead, replay
- 2xx dışı yanıt, zaman aşımı (10 sn), ağ hatası veya yönlendirme (3xx izlenmez) başarısızlıktır.
- Bekleme planı: 1 dk, 5 dk, 30 dk, 2 sa, 6 sa, 24 sa. İlk istek + 6 yeniden deneme (toplam 7 istek) başarısız olursa teslim
deadolur. - Dashboard veya API ile tekil (
POST …/webhooks/deliveries/{id}/replay) ya da toplu (POST …/webhooks/{id}/replay, başarısız/ölü, en fazla 1000) yeniden gönderim yapılabilir. Replay aynıPushizo-Event-Idile gider. - Endpoint sağlığı: art arda başarısızlık serisi 7 günü geçerse endpoint
failingolur ve organizasyon sahip/yöneticilerine e-posta gider; 14 günü geçersedisabledolur ve bekleyen teslimler kapatılır. Endpoint'i düzelttikten sonra etkinleştirip ölü teslimleri toplu replay edin. Herhangi bir başarılı teslim seriyi sıfırlar. - Adil teslim: bir tarama turunda endpoint başına sınırlı sayıda teslim alınır ve endpoint başına en fazla 8 eşzamanlı istek yapılır. Bir endpoint'e istek başarısız olursa o turdaki kalan teslimleri gönderilmez; deneme sayılmadan yaklaşık 30 sn sonra yeniden denenir. Böylece yavaş veya çökmüş bir alıcı diğer endpoint'leri geciktirmez.
- Birden fazla worker güvenle paralel çalışır: teslim, talep eden worker'ın kira süresi içinde tamamlanamayacaksa gönderilmez (aynı teslimin iki kez gönderilmesi önlenir).
- Teslim kayıtları 30 gün saklanır. Test gönder
webhook.testolayını anında gönderir ve sonucu gösterir (yeniden denenmez).
Kişisel veri (KVKK/GDPR)
- Kullanıcı silindiğinde (
DELETE /users/by/…) o kullanıcıya ait tüm webhook teslim kayıtları (gönderilmiş veya bekleyen; kullanıcı, alias, tag/özellik, olay ve dönüşüm yükleri) silinir; alıcıya yalnızcauser.deletedbildirimi gider. - App'in "Verileri temizle" işlemi webhook teslim geçmişini de siler (endpoint yapılandırması korunur).
İzleme
webhook_deliveries_total{status}, webhook_delivery_latency_seconds (olaydan başarılı teslime; SLO p95 < 30 sn) ve webhook_request_duration_seconds{outcome} metrikleri /metrics üzerinden yayımlanır. Yerel ölçüm: node tests/load/webhooks-bench.mjs --key … --app ….
Güvenlik (SSRF)
- Yalnızca
https://URL'leri kabul edilir; URL içinde kullanıcı adı/parola,localhost/.local/.internalgibi yerel adlar ve özel/ayrılmış IP adresleri reddedilir. - Gönderimde ana makine adı DNS ile çözülür; çözülen adreslerden biri bile özel/ayrılmışsa istek yapılmaz ve bağlantı denetlenen adrese sabitlenir (DNS rebinding koruması).
- Yerel geliştirmede kendi makinenizdeki alıcıyla deneme için
.enviçindeWEBHOOK_ALLOW_INSECURE_TARGETS=true(http ve yerel adreslere izin) verilebilir; bu ayarNODE_ENV=productioniken açılamaz.