Hızlı başlangıç
Anahtarlar, istek yardımcısı ve ilk gönderimleriniz; curl, PHP, Node.js ve Python ile.
Geliştiriciler
UPPU her olayı HTTPS uç noktanıza imzalı bir POST olarak gönderir: her istekte tek olay, en az bir kez ve kişi başına sıralı. İmzayı ham gövdeyle doğrulayın, tekrarları ayıklayın ve hızlıca 2xx dönün.
91.216.36.26Uç noktaları firma sahibi panelde, ekip ayarlarından ve parola onayıyla yönetir. Bir firmada en çok 5 uç nokta olabilir. Firmanızda API'nin açık olması gerekir.
443 numaralı porttan yayın yapan, genel bir alan adına sahip bir https adresi. Olayları seçin (varsayılan seçili olanlar: message.received, message.status, contact.identity_changed, contact.erased, channel.status_changed), isterseniz kanal süzgeci, sırayı (varsayılan best_effort ya da strict) ve max_in_flight değerini (1-32, varsayılan 8).
İmza sırrı, whsec_ ve ardından 32 baytın base64 hâli, yalnızca bir kez gösterilir. Sır kasanıza koyun.
Her isteği ayrıştırmadan önce doğrulayın (kod aşağıda). İmzası bozuk istek 401 alır.
«Test olayı gönder», "test": true taşıyan imzalı bir örnek gönderir. Uç noktanız 2xx dönmelidir.
«Yanlış imzalı deneme», yanlış bir sırla imzalanmış bir webhook.test gönderir. Uç noktanız 2xx dışında bir durum dönmelidir; bağlantı hatası sayılmaz.
İki denetimin de son 60 dakika içinde geçmiş olması gerekir. Etkinleştirmeden önce kaydedilen olaylar bu uç noktaya gönderilmez: onları GET /v1/events ile çekin.
Sırrı yenileme: panelde sırrı yenilediğinizde («Sırrı yenile») eski sır 24 saat daha imzalamaya devam eder ve her istek iki imzayı birden taşır, önce yenisi. Alıcınızı bu süre içinde yeni sırra geçirin.
| Başlık | Değer |
|---|---|
Content-Type | application/json; charset=utf-8 |
User-Agent | UPPU-Webhooks/1.0 |
webhook-id | Olay kimliği (ULID). Her denemede, yeniden gönderimde ve her uç noktada aynıdır; gövdedeki id ile aynıdır |
webhook-timestamp | Unix saniyesi; her denemede yeniden alınır |
webhook-signature | v1,<base64>; sır yenilenirken tek boşlukla v1,<yeni> v1,<eski> |
Uppu-Event-Type | Olay türü, ör. message.status |
Uppu-Delivery-Id | Bu uç noktaya yapılan teslimat; yeniden gönderimde aynı kalır |
Uppu-Attempt | 1'den başlar; yeniden gönderimde yine 1'den başlar |
data içindeki üst düzey değerler kalır (uzun metin kısaltılır); iç nesneler {"id": …} hâline iner ya da düşer ve "truncated": true eklenir. Bu durumda tam mesajı GET /v1/messages/{id} ile alın.91.216.36.26 adresinden gelir; değişiklik 30 gün önce duyurulur. Asıl güvenlik denetiminiz IP adresi değil imza olsun.3xx başarısızlık sayılır.Her olayın zarfı aynıdır; data API'nin kaynaklarını kullanır. Aşağıdaki, uydurma kimliklerle bir message.received olayıdır.
{"id": "01jace0e1v2m4n6p8q0r2s4t6a",
"object": "event",
"type": "message.received",
"api_version": "v1",
"created_at": "2026-10-20T09:15:01.020Z",
"team_id": "01j8x7tm5k2m4n6p8q0r2s4t6v",
"channel": {"id": "01j9z0ch5k2m4n6p8q0r2s4t6v", "type": "whatsapp"},
"test": false,
"data": {
"message": {"object": "message", "id": "01jab3t6ms2m4n6p8q0r2s4t6v",
"channel_id": "01j9z0ch5k2m4n6p8q0r2s4t6v", "channel_type": "whatsapp",
"conversation_id": "01jab39zcv2m4n6p8q0r2s4t6v",
"contact_id": "01j8x7ct5k2m4n6p8q0r2s4t6v", "direction": "in",
"author": {"type": "contact", "label": "Ayşe Y.", "api_key_prefix": null},
"content": {"type": "text", "text": {"body": "Merhaba, siparişim nerede?"}},
"context": null, "template": null, "preview": "Merhaba, siparişim nerede?",
"status": "received", "status_changed_at": null, "error": null,
"client_reference": null, "provider_message_id": "wamid.HBgMOTA1NTUwMDAwMDAxFQIAEhgg",
"sent_at": "2026-10-20T09:15:00.000Z", "received_at": "2026-10-20T09:15:01.000Z",
"created_at": "2026-10-20T09:15:01.000Z"},
"contact": {"id": "01j8x7ct5k2m4n6p8q0r2s4t6v", "display_name": "Ayşe Y.",
"identities": {"bsuid": "TR.13491208655302741918", "phone": "+905550000001"}},
"conversation": {"id": "01jab39zcv2m4n6p8q0r2s4t6v", "status": "open", "handling": "api"},
"service_window": {"open": true, "closes_at": "2026-10-21T09:13:00.000Z"}}}contact.id ile tutun. WhatsApp kullanıcı kimliği değişse de aynı kalır; telefon numarası hiç gelmeyebilir.template.updated olayında channel null'dır. test yalnızca panelin test olaylarında true'dur: 2xx dönün ve iş yapmayın.service_window olay kaydedilirken hesaplanır. Olayı sonradan işliyorsanız closes_at'i şimdiki zamanla karşılaştırın.| Tür | Ne zaman | Varsayılan açık |
|---|---|---|
message.received | Gelen her mesajda, web sohbeti dahil | Evet |
message.status | Giden mesajın her ileri durum değişiminde (accepted, sent, delivered, read, failed, unknown); previous_status ve sizin client_reference değerinizle | Evet |
message.created | Temsilcinin, sistemin ya da API'nin her giden mesajında | Hayır |
contact.identity_changed | WhatsApp kullanıcı kimliği (BSUID) değiştiğinde | Evet |
contact.consent_changed | Bir izin kaydedildiğinde ya da geri çekildiğinde | Hayır |
contact.erased | Bir kişi KVKK araçlarıyla silindiğinde | Evet |
template.updated | Bir şablonun durumu, kategorisi ya da kalitesi değiştiğinde | Hayır |
channel.status_changed | Kanalın durumu ya da Meta kipi değiştiğinde | Evet |
channel.health_changed | Kanal kontrol listesinin sağlık durumu değiştiğinde | Hayır |
webhook.test | Panelin test olayı ve yanlış imzalı deneme | Her zaman |
message.status'ta durumlar yalnızca ileri gider: queued, sonra accepted ya da unknown, sent, failed, delivered, read. Bir durumu yalnızca elinizdekinden üst sıradaysa uygulayın; failed sonradan delivered olabilir. Messenger'da sent gelmez; Instagram yalnızca read bildirir.
UPPU Standard Webhooks şemasıyla imzalar. Her şeyden önce imzayı ham gövdeyle doğrulayın.
whsec_ önekini atın, kalanı base64 ile çözün. Anahtar bu ham baytlardır, sırrın metni değil.webhook-id + "." + webhook-timestamp + "." + ham gövde. JSON'u önce çözüp yeniden kodlamayın.v1, değerlerinden biri sabit zamanlı karşılaştırmada tutuyorsa kabul edin.webhook-id'yi, yalnızca rakamlardan oluşmayan zaman damgasını ve saatinizden 300 saniyeden fazla uzak olanı (sunucu saatinizi NTP ile eşit tutun).webhook-id ile ayıklayın (kimlikleri en az 35 gün saklayın), işi kuyruğa alın ve 10 saniye içinde 2xx dönün.PHP
<?php
// PHP 8+. UPPU webhook v1 (Standard Webhooks): ham gövde, ham anahtar baytları, sabit zamanlı karşılaştırma.
function uppu_verify_webhook(array $secrets, string $id, string $timestamp, string $body,
string $signatureHeader, int $tolerance = 300, ?int $now = null): bool
{
if ($id === '' || !ctype_digit($timestamp) || abs(($now ?? time()) - (int) $timestamp) > $tolerance) {
return false;
}
$signed = $id . '.' . $timestamp . '.' . $body;
foreach ($secrets as $secret) {
if (!is_string($secret) || strncmp($secret, 'whsec_', 6) !== 0) {
continue;
}
$key = base64_decode(substr($secret, 6), true); // ham baytlar, metin değil
if ($key === false || $key === '') {
continue;
}
$expected = base64_encode(hash_hmac('sha256', $signed, $key, true));
foreach (explode(' ', $signatureHeader) as $part) {
[$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $signature)) {
return true;
}
}
}
return false;
}
// Alıcı. Apache ve nginx + PHP-FPM "webhook-id"yi $_SERVER['HTTP_WEBHOOK_ID'] olarak verir.
$header = fn (string $name): string => (string) ($_SERVER['HTTP_' . strtoupper(str_replace('-', '_', $name))] ?? '');
$body = (string) file_get_contents('php://input'); // ham gövde, değiştirilmeden
$secrets = array_filter([getenv('UPPU_WEBHOOK_SECRET') ?: '', getenv('UPPU_WEBHOOK_SECRET_OLD') ?: '']);
if (!uppu_verify_webhook($secrets, $header('webhook-id'), $header('webhook-timestamp'), $body, $header('webhook-signature'))) {
http_response_code(401); // yanlış imzalı deneme 2xx dışı bir yanıt bekler
exit;
}
$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// 1) tekrarı $event['id'] ile ayıklayın 2) işi kuyruğa alın 3) hızlıca yanıt verin
http_response_code(204);Node.js
// Node.js 18+, bağımlılık yok. Ham gövdeyi her JSON ayrıştırıcıdan önce okuyun.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyUppuWebhook(secrets, id, timestamp, rawBody, signatureHeader,
toleranceSec = 300, nowSec = Date.now() / 1000) {
if (!id || !/^[0-9]+$/.test(timestamp ?? '')) return false;
if (Math.abs(nowSec - Number(timestamp)) > toleranceSec) return false;
const signed = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const given = (signatureHeader ?? '').split(' ').flatMap((part) => {
const comma = part.indexOf(',');
return comma > 0 && part.slice(0, comma) === 'v1' ? [Buffer.from(part.slice(comma + 1), 'utf8')] : [];
});
for (const secret of secrets) {
if (typeof secret !== 'string' || !secret.startsWith('whsec_')) continue;
const key = Buffer.from(secret.slice(6), 'base64'); // ham baytlar, metin değil
if (key.length === 0) continue;
const expected = Buffer.from(createHmac('sha256', key).update(signed).digest('base64'), 'utf8');
if (given.some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected))) return true;
}
return false;
}
const secrets = [process.env.UPPU_WEBHOOK_SECRET, process.env.UPPU_WEBHOOK_SECRET_OLD].filter(Boolean);
createServer((req, res) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const raw = Buffer.concat(chunks); // ham baytlar; yeniden serileştirmeyin
const ok = verifyUppuWebhook(secrets, req.headers['webhook-id'], req.headers['webhook-timestamp'],
raw, req.headers['webhook-signature']);
if (!ok) { res.writeHead(401).end(); return; } // yanlış imzalı deneme 2xx dışı bir yanıt bekler
const event = JSON.parse(raw.toString('utf8'));
// tekrarı event.id ile ayıklayın, işi kuyruğa alın, sonra hızlıca yanıt verin
res.writeHead(204).end();
});
}).listen(8080); // HTTPS ters vekil sunucunuzun arkasında (port 443)Python
# Python 3.9+, yalnızca standart kitaplık.
import base64
import hashlib
import hmac
import json
import os
import re
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
def verify_uppu_webhook(secrets, msg_id, timestamp, raw_body, signature_header, tolerance=300, now=None):
if not msg_id or not timestamp or not re.fullmatch(r"[0-9]+", timestamp):
return False
if abs((time.time() if now is None else now) - int(timestamp)) > tolerance:
return False
signed = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
given = []
for part in (signature_header or "").split(" "):
version, _, signature = part.partition(",")
if version == "v1" and signature:
given.append(signature.encode("utf-8"))
for secret in secrets:
if not secret or not secret.startswith("whsec_"):
continue
key = base64.b64decode(secret[len("whsec_"):]) # ham baytlar, metin değil
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest())
if any(hmac.compare_digest(expected, signature) for signature in given):
return True
return False
class Receiver(BaseHTTPRequestHandler):
secrets = [s for s in (os.environ.get("UPPU_WEBHOOK_SECRET"), os.environ.get("UPPU_WEBHOOK_SECRET_OLD")) if s]
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) # ham baytlar; yeniden serileştirmeyin
ok = verify_uppu_webhook(self.secrets, self.headers.get("webhook-id"),
self.headers.get("webhook-timestamp"), raw, self.headers.get("webhook-signature"))
if not ok:
self.send_response(401) # yanlış imzalı deneme 2xx dışı bir yanıt bekler
self.end_headers()
return
event = json.loads(raw)
# tekrarı event["id"] ile ayıklayın, işi kuyruğa alın, sonra hızlıca yanıt verin
self.send_response(204)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("127.0.0.1", 8080), Receiver).serve_forever() # HTTPS ters vekil sunucunuzun arkasında (port 443)Doğrulayıcınızı bu değerlerle çevrimdışı sınayın; “şimdi” olarak zaman damgasını kullanın. Sırlar bu sayfa için uydurulmuştur, gerçek sır değildir. Yukarıdaki Python kodu sitenin her derlemesinde altı durumun hepsini geçer.
| Öğe | Değer |
|---|---|
| Sır | whsec_dXBwdS1leGFtcGxlLXNpZ25pbmcta2V5LTMyYnl0ZXM=Çözülünce 32 bayt olur: uppu-example-signing-key-32bytes. |
| Eski sır | whsec_dXBwdS1leGFtcGxlLU9MRC1zaWduaW5nLWtleS0zMmI= |
| webhook-id | 01jab3t6y8q7z0k5w9x2c4d6ef |
| webhook-timestamp | 17923149022026-10-18 09:15:02 UTC; “şimdi” olarak bunu kullanın. |
| Gövde | {"id":"01jab3t6y8q7z0k5w9x2c4d6ef","object":"event","type":"webhook.test","api_version":"v1","created_at":"2026-10-18T09:15:02.481Z","team_id":"01j8x7tm5k2m4n6p8q0r2s4t6v","channel":null,"test":true,"data":{"note":"UPPU test olayı","endpoint_id":"01jab3ep5k2m4n6p8q0r2s4t6v"}}UTF-8'de 277 bayt («olayı»daki ı iki bayttır). Sonunda satır sonu olmadan, aynen kopyalayın. |
| Beklenen imza | v1,jSu7JaoTMNvhCH/kppizvNNG0V/GD1DA6WSpY4cuxPw= |
| Yenileme sırasındaki başlık | v1,jSu7JaoTMNvhCH/kppizvNNG0V/GD1DA6WSpY4cuxPw= v1,mVpouHP8zRAeVBqhKcwVxioDhjcztU/vb3NHRPka6ms=Hem sır hem eski sır doğrular. |
webhook-id.webhook-id ile ayıklayın.max_in_flight kadar paralel gider.best_effort (varsayılan): yeniden denemeyi bekleyen olay sonrakileri bekletmez; olayları sırasız görebilirsiniz. strict: bitmemiş en eski olay o kişiyi tıkar. Bot durum makineleri için strict seçin; bedeli, tek bir hatalı olayın o kişiyi saatlerce bekletebilmesidir.2xx'tir. 410 uç noktayı hemen askıya alır. Diğer her yanıt, 3xx, 4xx ve zaman aşımı dahil, tekrar denenir. 429 ve 503 Retry-After taşıyabilir (en çok 1 saate kadar uyulur), ama yine de bir deneme hakkı harcar.Her denemeden önce bekleme (12 deneme, yaklaşık 76 saat, her bekleme ±%10). Son denemeden sonra teslimat dead olur.
failing yapar; sonra dakikada bir deneme yapılır ve ilk 2xx uç noktayı geri açar. 72 saat failing kalan ya da 410 dönen uç nokta suspended olur: teslimatlar en çok 7 gün bekletilir. Firma sahibine ve yöneticilere e-posta gider.webhook-id ve aynı baytlar, yeni bir zaman damgasıyla. Gövdeler teslim edilen olayda 7 gün, dead ya da bekletilen olayda en çok 30 gün saklanır.429 ya da 503 UPPU'yu yavaşlatır; kalıcı yük için hemen 2xx dönün, işi arkada yapın ve max_in_flight'ı düşürün.Bir kesintiden sonra ya da uç nokta etkin değilken kaydedilen olaylar için aynı olayları API'den çekin (kapsam events:read, saniyede 2 istek).
after (işlediğiniz son olay ya da önceki sayfanın next_cursor'ı), types=a,b ve limit (1-100).after 422 alır (alan after): after olmadan baştan çekin.template.updated için bütün kanalların anahtarı gerekir.cURL
curl -sS "$UPPU_API_BASE/events?after=01jace0e1v2m4n6p8q0r2s4t6a&types=message.received,message.status&limit=100" \
-H "Authorization: Bearer $UPPU_API_KEY"PHP
// Hızlı başlangıçtaki uppu_request()'i kullanır; load/save/handle sizin işlevlerinizdir.
$after = load_last_event_id(); // ilk seferde ''
while (true) {
$query = http_build_query(array_filter(['after' => $after, 'types' => 'message.received,message.status', 'limit' => 100]));
$r = uppu_request('GET', 'events?' . $query);
if ($r['status'] === 422 && $after !== '') { // bilinmiyor ya da 7 günden eski: baştan
$after = '';
continue;
}
if ($r['status'] !== 200) {
break; // sonra deneyin (429: Retry-After kadar sonra)
}
foreach ($r['body']['data'] as $event) {
handle_event_once($event); // alıcınızdaki tekrar ayıklamasının aynısı
$after = $event['id'];
}
save_last_event_id($after);
if ($r['body']['next_cursor'] === null) {
break;
}
usleep(600000); // saniyede 2 isteğin altında kalın
}Node.js
// Hızlı başlangıçtaki uppuRequest()'i kullanır; load/save/handle sizin işlevlerinizdir.
let after = await loadLastEventId(); // ilk seferde ''
for (;;) {
const query = new URLSearchParams({ types: 'message.received,message.status', limit: '100' });
if (after) query.set('after', after);
const { status, data } = await uppuRequest('GET', `events?${query}`);
if (status === 422 && after) { after = ''; continue; } // bilinmiyor ya da 7 günden eski: baştan
if (status !== 200) break; // sonra deneyin (429: Retry-After kadar sonra)
for (const event of data.data) {
await handleEventOnce(event); // alıcınızdaki tekrar ayıklamasının aynısı
after = event.id;
}
await saveLastEventId(after);
if (data.next_cursor === null) break;
await new Promise((resolve) => setTimeout(resolve, 600)); // saniyede 2 isteğin altında kalın
}Python
# Hızlı başlangıçtaki uppu_request()'i kullanır; load/save/handle sizin işlevlerinizdir.
import time
from urllib.parse import urlencode
after = load_last_event_id() # ilk seferde ""
while True:
params = {"types": "message.received,message.status", "limit": 100}
if after:
params["after"] = after
status, page = uppu_request("GET", "events?" + urlencode(params))
if status == 422 and after: # bilinmiyor ya da 7 günden eski: baştan
after = ""
continue
if status != 200:
break # sonra deneyin (429: Retry-After kadar sonra)
for event in page["data"]:
handle_event_once(event) # alıcınızdaki tekrar ayıklamasının aynısı
after = event["id"]
save_last_event_id(after)
if page["next_cursor"] is None:
break
time.sleep(0.6) # saniyede 2 isteğin altında kalın"state": "pending" ile gelir. Dosyayı GET /v1/media/{id}/content ile indirin (kapsam media:read); 409 media_not_ready yanıtı Retry-After: 2 taşır. Dosyanın hazır olduğunu bildiren ayrı bir olay yoktur; bekleyen dosyaları arka planda tamamlayın. ETag ve tek aralıklı Range istekleri desteklenir.attachments altında gelebilir.data.service_window'a bakın. Pencere Meta'nın saatiyle sayılır; Meta'nın geç teslim ettiği mesaj "open": false ile gelebilir. WhatsApp'ta onaylı bir şablon gönderin; Messenger ve Instagram'da kişinin yeniden yazmasını bekleyin.401; sır yenilenirken iki imza da kabul edilir.webhook-id ile ayıklanır ve kimlikler en az 35 gün saklanır; tekrar gelen olay da 2xx alır.2xx, iş kuyrukta; askıya almak istemiyorsanız 410 yok."test": true olaylarına yanıt verilir ama iş yapılmaz.contact.id ile tutulur; contact.identity_changed'de BSUID güncellenir; contact.erased'de sizdeki kişisel veri silinir.GET /v1/events ile doldurulur.Anahtarlar, istek yardımcısı ve ilk gönderimleriniz; curl, PHP, Node.js ve Python ile.
Event şeması, bütün uç noktalar ve webhook isteği; OpenAPI belgesinden üretilir.
Eşzamanlı ve asenkron kodlar; message.status olayının taşıdıkları da dahil.
API'yi ve webhook'ları firmanız için açar, kendi senaryonuzla birlikte üzerinden geçeriz.