İçeriğe geç

Geliştiriciler

Webhook'lar: imzalı, sıralı ve tekrar denenen olaylar.

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.

  • Standard Webhooks v1 · HMAC-SHA256
  • Yaklaşık 76 saatte 12 deneme
  • Çıkış adresi 91.216.36.26

Uç noktayı kurun

Uç 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.

  1. Uç noktayı oluşturun

    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).

  2. Sırrı saklayın

    İmza sırrı, whsec_ ve ardından 32 baytın base64 hâli, yalnızca bir kez gösterilir. Sır kasanıza koyun.

  3. Alıcınızı imza doğrulamasıyla yayına alın

    Her isteği ayrıştırmadan önce doğrulayın (kod aşağıda). İmzası bozuk istek 401 alır.

  4. Test olayı gönderin

    «Test olayı gönder», "test": true taşıyan imzalı bir örnek gönderir. Uç noktanız 2xx dönmelidir.

  5. Yanlış imzalı denemeyi çalıştırın

    «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.

  6. Etkinleştirin

    İ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.

İsteğin biçimi

İstek başlıkları
BaşlıkDeğer
Content-Typeapplication/json; charset=utf-8
User-AgentUPPU-Webhooks/1.0
webhook-idOlay kimliği (ULID). Her denemede, yeniden gönderimde ve her uç noktada aynıdır; gövdedeki id ile aynıdır
webhook-timestampUnix saniyesi; her denemede yeniden alınır
webhook-signaturev1,<base64>; sır yenilenirken tek boşlukla v1,<yeni> v1,<eski>
Uppu-Event-TypeOlay türü, ör. message.status
Uppu-Delivery-IdBu uç noktaya yapılan teslimat; yeniden gönderimde aynı kalır
Uppu-Attempt1'den başlar; yeniden gönderimde yine 1'den başlar
  • Her istekte tek olay, en çok 262.144 baytlık sıkı UTF-8 JSON olarak gelir. Daha büyük olayda 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.
  • İstekler 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.
  • UPPU en çok 5 saniyede bağlanır ve yanıtınızı en çok 10 saniye bekler. Yönlendirme izlenmez: 3xx başarısızlık sayılır.
  • Gövde olay kaydedilirken bir kez serileştirilir ve her denemede bayt bayt aynı gider.

Olay zarfı

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.

message.received
{"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"}}}
  • Kişiyi 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.
  • Tanımadığınız olay türlerini ve alanları yok sayın: v1 içinde yenileri eklenebilir.

Olay türleri

Olay türleri
TürNe zamanVarsayılan açık
message.receivedGelen her mesajda, web sohbeti dahilEvet
message.statusGiden mesajın her ileri durum değişiminde (accepted, sent, delivered, read, failed, unknown); previous_status ve sizin client_reference değerinizleEvet
message.createdTemsilcinin, sistemin ya da API'nin her giden mesajındaHayır
contact.identity_changedWhatsApp kullanıcı kimliği (BSUID) değiştiğindeEvet
contact.consent_changedBir izin kaydedildiğinde ya da geri çekildiğindeHayır
contact.erasedBir kişi KVKK araçlarıyla silindiğindeEvet
template.updatedBir şablonun durumu, kategorisi ya da kalitesi değiştiğindeHayır
channel.status_changedKanalın durumu ya da Meta kipi değiştiğindeEvet
channel.health_changedKanal kontrol listesinin sağlık durumu değiştiğindeHayır
webhook.testPanelin test olayı ve yanlış imzalı denemeHer 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.

İmzayı doğrulayın

UPPU Standard Webhooks şemasıyla imzalar. Her şeyden önce imzayı ham gövdeyle doğrulayın.

  1. Anahtar: sırdaki whsec_ önekini atın, kalanı base64 ile çözün. Anahtar bu ham baytlardır, sırrın metni değil.
  2. İmzalanan içerik: webhook-id + "." + webhook-timestamp + "." + ham gövde. JSON'u önce çözüp yeniden kodlamayın.
  3. İmza: imzalanan içeriğin HMAC-SHA256'sı, standart ve dolgulu base64 olarak.
  4. Karşılaştırma: başlığı tek boşluklardan bölün; v1, değerlerinden biri sabit zamanlı karşılaştırmada tutuyorsa kabul edin.
  5. Reddedin: boş 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).
  6. Sonra: tekrarları 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);

Deneme vektörü

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.

İmza deneme vektörü
ÖğeDeğer
Sırwhsec_dXBwdS1leGFtcGxlLXNpZ25pbmcta2V5LTMyYnl0ZXM=Çözülünce 32 bayt olur: uppu-example-signing-key-32bytes.
Eski sırwhsec_dXBwdS1leGFtcGxlLU9MRC1zaWduaW5nLWtleS0zMmI=
webhook-id01jab3t6y8q7z0k5w9x2c4d6ef
webhook-timestamp17923149022026-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 imzav1,jSu7JaoTMNvhCH/kppizvNNG0V/GD1DA6WSpY4cuxPw=
Yenileme sırasındaki başlıkv1,jSu7JaoTMNvhCH/kppizvNNG0V/GD1DA6WSpY4cuxPw= v1,mVpouHP8zRAeVBqhKcwVxioDhjcztU/vb3NHRPka6ms=Hem sır hem eski sır doğrular.
  • Geçmeli: sırla tek imza; sırla yenileme sırasındaki başlık; eski sırla yenileme sırasındaki başlık.
  • Kalmalı: sonuna bir boşluk eklenmiş gövde; “şimdi” olarak zaman damgası artı 301 saniye; boş webhook-id.

Teslim, sıra ve yeniden deneme

  • En az bir kez: aynı olay iki kez gelebilir, ör. bir zaman aşımından sonra. Tekrarları webhook-id ile ayıklayın.
  • Kişi başına sıralı: bir kişinin olayları (şablon ve kanal olaylarında: bir şablonun ya da kanalın olayları) ilk giren ilk çıkar sırasıyla ve aynı anda en çok bir tane teslim edilir. Farklı kişiler 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.
  • Başarı yalnızca 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.

  1. 0ilk deneme
  2. 30 sn2. deneme
  3. 2 dk3. deneme
  4. 10 dk4. deneme
  5. 30 dk5. deneme
  6. 1 sa6. deneme
  7. 2 sa7. deneme
  8. 4 sa8. deneme
  9. 8 sa9. deneme
  10. 12 sa10. deneme
  11. 24 sa11. deneme
  12. 24 sason deneme
  • Devre kesici: art arda 20 başarısızlık uç noktayı 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.
  • Panelden yeniden gönderim: aynı 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.
  • Kısa süreli 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.

Kaçırılan olaylar: GET /v1/events

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).

  • Son 7 gün, eskiden yeniye, webhook'larla aynı kimlik ve gövdeyle. Aynı tekrar ayıklamasından geçirin.
  • Parametreler: after (işlediğiniz son olay ya da önceki sayfanın next_cursor'ı), types=a,b ve limit (1-100).
  • Son 10 saniyenin olayları sonraki bir istekte görünür; imleç böylece hiçbir olayı atlamaz. Test olayları hiç listelenmez.
  • Bilinmeyen ya da 7 günden eski bir after 422 alır (alan after): after olmadan baştan çekin.
  • Tek kanala bağlı anahtar yalnızca o kanalın olaylarını görür; template.updated için bütün kanalların anahtarı gerekir.
GET/v1/events

cURL

curl -sS "$UPPU_API_BASE/events?after=01jace0e1v2m4n6p8q0r2s4t6a&types=message.received,message.status&limit=100" \
  -H "Authorization: Bearer $UPPU_API_KEY"

Medya ve yanıt penceresi

  • Gelen medya, dosya inerken UPPU'nun medya kimliği ve "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.
  • Gelen medya 90 gün saklanır. Messenger ve Instagram'da bir mesajda birden çok dosya attachments altında gelebilir.
  • Yanıt penceresi: otomatik serbest metin yanıtından önce 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.

Alıcı kontrol listesi

  • Geçerli sertifikalı, 443 numaralı porttan yayın yapan, yönlendirmesiz genel bir HTTPS uç noktası.
  • İmza ayrıştırmadan önce ham gövdeyle denetlenir; bozuk imzaya 401; sır yenilenirken iki imza da kabul edilir.
  • Tekrarlar webhook-id ile ayıklanır ve kimlikler en az 35 gün saklanır; tekrar gelen olay da 2xx alır.
  • Birkaç saniye içinde 2xx, iş kuyrukta; askıya almak istemiyorsanız 410 yok.
  • Durumlar sıraya göre uygulanır, asla geri gitmez; bilinmeyen olay türleri ve alanlar yok sayılır; "test": true olaylarına yanıt verilir ama iş yapılmaz.
  • Kişiler contact.id ile tutulur; contact.identity_changed'de BSUID güncellenir; contact.erased'de sizdeki kişisel veri silinir.
  • Kesintiden sonra boşluklar GET /v1/events ile doldurulur.

Sonraki adımlar

UPPU'nun olaylarını sistemlerinize bağlayın.

API'yi ve webhook'ları firmanız için açar, kendi senaryonuzla birlikte üzerinden geçeriz.

  • Standard Webhooks
  • Kişi başına sıralı
  • 7 günlük olay yedeği