Webhooklar, WhatsApp tarafında olan olayları anında sunucunuza bildirir: müşteriden gelen mesaj, teslim ve okunma durumları, şablon onayı, numara ve kredi uyarıları. Bu sayfa aboneliği kurmayı, imzayı doğrulamayı ve teslimleri güvenilir biçimde işlemeyi anlatır.
#Nasıl çalışır
- Bir olay gerçekleşir (ör. müşteri mesaj gönderir).
- Veri Merkezi olayı abone olduğunuz adrese
POSTile gönderir. Meta kaynaklı olaylarda (mesaj, durum, arama, form) gecikme genellikle 1 saniyenin altındadır; Meta dışından tetiklenen olaylar (credit.*,number.detached,number.purged, paneldencontact.opt_out_changed, kampanya ya da otomasyonmessage.sent,privacy.erasure_completed) dakikalık kuyrukla en geç yaklaşık 1 dakikada gider. - Sunucunuz 10 saniye içinde
2xxdöndürür. 2xxdönmezse teslim artan aralıklarla yeniden denenir.
#1. Alıcı uç noktanızı hazırlayın
Uç noktanız:
- Herkese açık bir
https://adres olmalıdır. İç ağ, özel, loopback ve link-local adresler reddedilir. - Yönlendirme (
3xx) yapmamalıdır; yönlendirmeler izlenmez. - Ham istek gövdesini imza doğrulaması için ayrıştırmadan okuyabilmelidir.
#2. Abonelik oluşturun
curl -X POST https://api.verimerkezi.app/wa/webhooks \
-H "Authorization: Bearer $VM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.example.com/wa-webhook",
"events": ["message.received", "message.status.delivered", "message.status.read", "message.status.failed", "template.approved", "template.rejected"],
"description": "Üretim botu"
}'const hook = await vm.createWebhook(
'https://api.example.com/wa-webhook',
['message.received', 'message.status.delivered', 'message.status.failed', 'credit.low'],
'Üretim botu'
);
console.log(hook.secret); // whsec_... yalnız şimdi dönerhook = vm.create_webhook(
'https://api.example.com/wa-webhook',
['message.received', 'message.status.delivered', 'message.status.failed', 'credit.low'],
'Üretim botu',
)
print(hook['secret']) # yalnız şimdi döner<?php
$hook = $vm->createWebhook(
'https://api.example.com/wa-webhook',
['message.received', 'message.status.delivered', 'message.status.failed', 'credit.low'],
'Üretim botu'
);
echo $hook['secret']; // yalnız şimdi dönersecret, whsec_ ile başlar) yalnız oluşturma yanıtında döner. Hemen güvenli bir yere kaydedin. Kaybederseniz POST /webhooks/{id}/rotate-secret ile yeni anahtar alın.- Aynı adrese ikinci bir aktif abonelik oluşturulamaz (
409 webhook_url_exists); mevcut aboneliğiPATCHile güncelleyin. - Bilinmeyen olay adı ya da boş liste
422 invalid_webhookdöner.
#* (tüm olaylar) seçeneği
events alanını göndermezseniz ya da ["*"] gönderirseniz abonelik, o anki 12 klasik olaya genişletilerek kaydedilir. Sonradan eklenen olaylar bu aboneliğe kendiliğinden gelmez. Aşağıdaki tabloda "Hayır" ile işaretli olayları açıkça seçmelisiniz.
#3. İmzayı doğrulayın
Her istek şu başlıkları taşır:
Content-Type: application/json
X-VeriMerkezi-Signature-256: sha256=5d2a...e91f
X-VeriMerkezi-Timestamp: 1791619200
X-VeriMerkezi-Event-Id: 01a0c7ea-0d80-7d4d-aaf0-a2cc64a6bb00
X-VeriMerkezi-Event-Type: message.received
X-VeriMerkezi-Delivery-Attempt: 1
User-Agent: VeriMerkezi-Webhook/1.0İmza şu formülle üretilir:
sha256= + HMAC_SHA256(anahtar, zaman_damgasi + "." + ham_govde)Doğrulama adımları:
- Ham gövdeyi okuyun.
X-VeriMerkezi-Timestamp5 dakikadan eskiyse isteği reddedin.- Beklenen imzayı hesaplayın ve başlıkla sabit zamanlı karşılaştırın.
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/wa-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.VM_WEBHOOK_SECRET;
const ts = req.header('X-VeriMerkezi-Timestamp') || '';
const sig = req.header('X-VeriMerkezi-Signature-256') || '';
const body = req.body.toString('utf8');
if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return res.status(401).end();
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + body).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.status(401).end();
const event = JSON.parse(body);
kuyrugaEkle(event); // işi arka plana alın
res.status(200).json({ ok: true });
});import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ['VM_WEBHOOK_SECRET'].encode()
@app.post('/wa-webhook')
def webhook():
ts = request.headers.get('X-VeriMerkezi-Timestamp', '')
sig = request.headers.get('X-VeriMerkezi-Signature-256', '')
body = request.get_data(as_text=True)
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
expected = 'sha256=' + hmac.new(SECRET, f'{ts}.{body}'.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
kuyruga_ekle(json.loads(body))
return {'ok': True}<?php
$secret = getenv('VM_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_VERIMERKEZI_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_VERIMERKEZI_SIGNATURE_256'] ?? '';
if (abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }
kuyrugaEkle(json_decode($body, true));
http_response_code(200);
echo json_encode(['ok' => true]);Resmî SDK'larda hazır doğrulama yardımcısı vardır: Node.js VeriMerkeziClient.verifyWebhookSignature(secret, body, signature, timestamp), Python VeriMerkeziClient.verify_webhook_signature(...).
#Olay yapısı
Her olay aynı zarfı kullanır:
{
"event": "message.received",
"event_id": "01a0c7ea-0d80-7d4d-aaf0-a2cc64a6bb00",
"event_type": "message.received",
"occurred_at": "2026-10-10T10:00:00+03:00",
"created_at": "2026-10-10 10:00:00",
"data": {
"wamid": "wamid.HBgMOTA1NTUxMjM0NTY3FQIAEhggQTNFQkQ2RjQ3MEIyQzk0RTE4AA==",
"from": "905550000000",
"user_id": null,
"username": null,
"name": "Ayşe Yılmaz",
"type": "text",
"text": "Merhaba, siparişim ne durumda?",
"media": null,
"timestamp": "1791619200",
"phone_number_id": "1234567890",
"contact": { "wa_id": "905550000000", "user_id": null, "username": null, "profile_name": "Ayşe Yılmaz" }
}
}| Alan | Açıklama |
|---|---|
event | Olay adı. |
event_id | Olayın benzersiz kimliği (UUIDv7). Yeniden denemelerde aynı kalır. |
event_type, created_at | event ve occurred_at alanlarının eş anlamlıları (geriye dönük uyumluluk). |
occurred_at | Olay zamanı, saat dilimi ofsetli ISO 8601. |
data | Olaya özgü içerik. |
data.text düz metindir; medyalı mesajda açıklamayı taşır. Dosya data.media.media_id ile indirilir. Buton yanıtlarında data.button alanı { payload, text } olarak dolar. Etkileşimli mesajlarda data.interactive alanı { type, name } taşır (ör. button_reply, list_reply; form yanıtında { "type": "nfm_reply", "name": "flow" }); düğme ve liste seçiminde ayrıca reply_id, reply_title ve (yalnız listede) reply_description gelir. Diğer mesajlarda nulldır.
#Olaylar
| Olay | Ne zaman | * kapsar |
|---|---|---|
message.received | Müşteri mesaj gönderdi. | Evet |
message.echo | Coexistence: işletme telefondaki uygulamadan yazdı. | Evet |
message.status.sent | Meta gönderimi kabul etti. | Evet |
message.status.delivered | Mesaj teslim edildi. | Evet |
message.status.read | Mesaj okundu. | Evet |
message.status.failed | Gönderim başarısız; errors alanına bakın. | Evet |
template.approved | Şablon onaylandı. | Evet |
template.rejected | Şablon reddedildi; reason / rejected_reason ve rejected_reason_text alanlarına bakın. | Evet |
template.flagged | Şablon işaretlendi. | Evet |
template.paused | Şablon durduruldu. | Evet |
quality.changed | Numara kalite puanı değişti. | Evet |
account.alert | Meta hesap uyarısı. | Evet |
message.revoked | Bir mesaj silindi. | Hayır |
message.edited | Bir mesaj düzenlendi. | Hayır |
message.sent | API dışından mesaj gönderildi (panel, otomasyon, kampanya). | Hayır |
message.history | Coexistence geçmiş aktarımından eski mesaj. | Hayır |
number.status_changed | Numara bağlandı, koptu, işaretlendi ya da kısıtlandı. | Hayır |
number.name_changed | Görünen ad için Meta kararı geldi. | Hayır |
number.detached | Numara hesabınızdan ayrıldı. | Hayır |
number.purged | Ayrılan numaranın kayıt silmesi tamamlandı. | Hayır |
credit.low | Kredi eşiğin altına düştü. | Hayır |
credit.exhausted | Kredi tükendi. | Hayır |
privacy.erasure_completed | KVKK silme talebi tamamlandı. | Hayır |
contact.opt_out_changed | Kişinin abonelik durumu değişti. | Hayır |
template.category_changed | Meta şablon kategorisini değiştirdi. | Hayır |
template.quality_changed | Şablon kalite puanı değişti. | Hayır |
template.disabled | Şablon Meta tarafından devre dışı bırakıldı. | Hayır |
template.reinstated | Duraklatma ya da işaret kalktı; şablon yeniden kullanılabilir. | Hayır |
call.connect | Gelen arama çalıyor ya da başlattığınız arama yanıtlandı. | Hayır |
call.status | Arama durumu değişti. | Hayır |
call.terminate | Arama bitti. | Hayır |
call.permission_reply | Müşteri arama izni isteğini yanıtladı. | Hayır |
flow.completed | Müşteri formu gönderdi. | Hayır |
flow.status_changed | Form durumu değişti. | Hayır |
contact.synced | Coexistence: telefondaki uygulama rehberinde kişi eklendi, düzenlendi ya da silindi (toplu). | Hayır |
message.status.deleted | Meta mesajın silindiğini bildirdi. | Hayır |
template.pending, template.in_appeal, template.locked | Şablon incelemede, itirazda ya da kilitli. | Hayır |
template.archived, template.unarchived | Şablon arşivlendi ya da arşivden çıktı. | Hayır |
template.deleted, template.pending_deletion | Şablon silindi ya da silinmek üzere. | Hayır |
template.limit_exceeded | Hesap şablon sınırına ulaştı. | Hayır |
number.limit_changed | Numaranın Meta mesaj limiti değişti. | Hayır |
account.review_changed | WhatsApp işletme hesabı incelemesi sonuçlandı. | Hayır |
Şablon durum olaylarının (template.approved, rejected, paused, flagged, disabled, reinstated) yükünde previous_status, rejected_reason, rejected_reason_text, quality_score, occurred_at ve source (webhook ya da sync) bulunur; eski reason alanı korunur. Meta bildirimi gelmediyse geçiş şablon senkronunda yakalanır; aynı geçiş iki kez yayınlanmaz. Hata ve uyarı taşıyan olaylarda (message.status.failed, şablon olayları, account.alert, number.name_changed, account.review_changed) Türkçe message_tr / reason_tr ve action_tr alanları gelir; Meta'nın ham alanları aynen korunur. Sözlüğün tamamı: GET /meta/error-codes.
Her olayın örnek yükü: Webhook olayları.
#Teslim durumunu eşleştirin
message.status.* olayları wamid, recipient, timestamp, errors, phone_number_id, conversation, pricing, biz_opaque_callback_data, user_id ve direction alanlarını taşır. Gönderimde Idempotency-Key kullandıysanız biz_opaque_callback_data bu anahtarı içerir; olayı kendi kaydınızla ek alan göndermeden eşleştirebilirsiniz.
#Yeniden deneme
| Deneme | Bekleme |
|---|---|
| 1 | Olay anında |
| 2 | +1 dakika |
| 3 | +5 dakika |
| 4 | +30 dakika |
| 5 | +2 saat |
| 6 | +12 saat |
| 7 | +24 saat |
- deneme de başarısız olursa teslim ölü (dead-letter) duruma geçer; toplam süre yaklaşık 38,6 saattir. Son 7 gündeki ölü teslimleri yeniden kuyruğa almak için:
curl -X POST https://api.verimerkezi.app/wa/webhooks/redeliver \
-H "Authorization: Bearer $VM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "since": "2026-10-08T00:00:00Z" }'event_id değerini saklayın ve daha önce işlediğiniz olayları atlayın.#Test edin
POST /webhooks/{id}/testaboneliğe anında imzalı birtest.pingolayı gönderir ve sonucu döndürür.- Test anahtarıyla
POST /test/inboundsahte bir gelen mesaj üretir. Bkz. Test modu.
#İmza anahtarını döndürün
Aboneliği silmeden imza anahtarını yenileyin:
curl -X POST https://api.verimerkezi.app/wa/webhooks/17/rotate-secret \
-H "Authorization: Bearer $VM_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "grace_seconds": 86400 }'Yeni anahtar yalnız bu yanıtta döner. grace_seconds süresince her teslimde birincil X-VeriMerkezi-Signature-256 başlığı yeni anahtarla, ek X-VeriMerkezi-Signature-256-Previous başlığı eski anahtarla imzalanır. Alıcınızı yeni anahtara geçirene kadar iki imzadan birini kabul edin; böylece hiçbir teslim düşmez.
#Aboneliği yönetin
| İşlem | Uç |
|---|---|
| Listele | GET /webhooks |
| Güncelle (adres, olaylar, etkinlik, ad) | PATCH /webhooks/{id} |
| Pasifleştir | PATCH /webhooks/{id} ile { "active": false } |
| Sil | DELETE /webhooks/{id} |
| Teslim sağlığı | GET /health |
Kaçan olayları mesaj kayıtlarından da geri alabilirsiniz: GET /messages. Bkz. Mesaj gönderme.
