Webhook

Webhook bildirimleri

Poll etmeyi bırakın. SMS kodu ulaştığı anda, imzalı ve tekrar denemeli olarak sunucunuza gönderiyoruz.

Neden webhook

Poll etmek hem istek harcar hem gecikme ekler. Webhook, kod düştüğü saniye sunucunuza ulaşır.

  • Poll döngüsü yok: günlük istek kotanız durum sorgusu yerine gerçek işe gider.
  • Daha düşük gecikme: kod bize ulaştığı anda sizin sisteminize de ulaşır, bir sonraki sorgunuzu beklemez.
  • Terminal olaylar da gelir: iptal ve süre aşımı bildirimleri iade durumuyla birlikte gelir, muhasebeniz senkron kalır.

Kurulum

API anahtarınızla hesap başına tek bir URL kaydedin. İmza secret'ı yalnız bir kez döner.

curl -X POST https://smsbulk.net/api/users/webhook \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://your-app.com/smsbulk/webhook"}'

Yanıt

{
  "id": "wep_2f8c1a9d",
  "url": "https://your-app.com/smsbulk/webhook",
  "sourceFilter": "API",
  "secret": "whsec_5f3a9c1e8b2d47a6903c5e1f7d8b4a26"
}

Secret'ı hemen saklayın

Düz secret yalnız bu yanıtta görünür. Sonraki okumalar maskeli değer döner. Kaybederseniz panelden veya rotate ucundan yenileyin.

URL şartları

  • 443 portunda HTTPS. Düz HTTP ve farklı portlar reddedilir.
  • Public bir adres. Özel aralıklar, loopback ve bulut metadata adresleri hem kayıtta hem her teslimatta reddedilir.
  • Yönlendirme yok. 3xx yanıtı takip edilmez, başarısız deneme sayılır.
MetotYolAmaç
POST/api/users/webhookUcu kaydeder veya değiştirir, secret'ı bir kez döner
GET/api/users/webhookMevcut ucu okur, secret maskelidir
PATCH/api/users/webhookURL veya kanal filtresini değiştirir, pasif ucu yeniden aktive eder
POST/api/users/webhook/rotateİmza secret'ını geçiş penceresiyle yeniler
POST/api/users/webhook/testGerçek aktivasyon verisi taşımayan test olayı gönderir
DELETE/api/users/webhookUcu siler, tüm bildirimleri durdurur
GET/api/users/webhook/deliveriesSon 20 teslimat denemesi, yalnız üstveri

Olaylar

Üç olay, hepsi tek bir aktivasyon hakkında.

OlayNe zaman gider
activation.code_receivedSMS kodu geldiğinde. Entegrasyonların çoğu bu olaya göre hareket eder.
activation.cancelledAktivasyon iptal edildiğinde. Gövde iptal sebebini ve bakiyenin iade edilip edilmediğini taşır.
activation.expiredAktivasyon kod gelmeden süresini doldurduğunda. Gövde iade durumunu taşır.

Kanal filtresi

Varsayılan olarak yalnız API ile verilen siparişler bildirim üretir. Panelden verdiğiniz siparişleri de istiyorsanız sourceFilter değerini null yapın.

İstek ve gövde

Her teslimat JSON gövdeli bir POST'tur. Alanlar tek tek seçilir: iç yönlendirme ve maliyet ayrıntıları hiçbir zaman yer almaz.

İstek şekli

POST /smsbulk/webhook HTTP/1.1
Content-Type: application/json
User-Agent: SMSBulk-Webhook/1
X-SMSBulk-Signature: t=1785312000,v1=8f2a...c41d
X-SMSBulk-Event-Id: evt_del_9f2c1a
X-SMSBulk-Event-Type: activation.code_received
X-SMSBulk-Delivery-Attempt: 1

activation.code_received

{
  "id": "evt_del_9f2c1a",
  "type": "activation.code_received",
  "created": 1785312000,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "RECEIVED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": "483920",
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "sms_text": "Your code is 483920",
    "received_at": "2026-08-04T09:12:34.000Z"
  }
}

activation.cancelled ve activation.expired

{
  "id": "evt_del_7b31de",
  "type": "activation.cancelled",
  "created": 1785312600,
  "data": {
    "activation_id": "cms2u6x80d1twmkudp2cfbqo4",
    "status": "CANCELLED",
    "phone_number": "447700900123",
    "service": "wa",
    "country": "GB",
    "code": null,
    "price": "0.42",
    "currency": "USD",
    "source": "API",
    "created_at": "2026-08-04T09:12:00.000Z",
    "reason": "user_cancel",
    "refunded": true,
    "refund_amount": "0.42"
  }
}

İleride yeni alanlar eklenebilir. Tanımadığınız alanlarda hata vermek yerine onları yok sayın.

İmza doğrulama

Her istek, zaman damgası ve ham gövdeden üretilen bir HMAC SHA256 imzası taşır.

X-SMSBulk-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
signed_string = "<t>.<raw_request_body>"

Ham byte üzerinden doğrulayın

İmzayı aldığınız gövdenin birebir kendisi üzerinden hesaplayın. JSON'u ayrıştırıp yeniden metne çevirirseniz anahtar sırası ve boşluklar değişir, imza tutmaz.

Birden fazla v1 gelebilir

Secret yenileme sırasında başlıkta iki v1 değeri bulunur. Değerleri sözlüğe değil listeye toplayın: sözlük yalnız sonuncuyu tutar ve yeni secret sessizce doğrulanmamış kalır.

Node.js

const crypto = require('crypto');

function verify(rawBody, header, secret) {
  const pairs = header.split(',').map((p) => {
    const i = p.indexOf('=');
    return [p.slice(0, i), p.slice(i + 1)];
  });

  const t = pairs.find(([k]) => k === 't')?.[1];
  // During a rotate window more than one signature arrives: collect them all.
  const signatures = pairs.filter(([k]) => k === 'v1').map(([, v]) => v);
  if (!t || signatures.length === 0) return false;

  // Replay window: reject anything older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return signatures.some(
    (s) =>
      s.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(s, 'hex'),
        Buffer.from(expected, 'hex'),
      ),
  );
}

Python

import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    pairs = [p.split("=", 1) for p in header.split(",")]

    t = next((v for k, v in pairs if k == "t"), None)
    # During a rotate window more than one signature arrives: collect them all.
    signatures = [v for k, v in pairs if k == "v1"]
    if t is None or not signatures:
        return False

    # Replay window: reject anything older than 5 minutes.
    if abs(time.time() - int(t)) > 300:
        return False

    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return any(hmac.compare_digest(s, expected) for s in signatures)

Zaman damgası imzalanan dizenin parçasıdır, birkaç dakikadan eski bildirimleri reddedebilirsiniz. İmzaları sabit zamanlı karşılaştırın.

Yanıt ve tekrar denemeler

Hızlı cevap verin. Yanıt için 10 saniye bekliyoruz.

Yapın

  • Olayı kaydeder kaydetmez 2xx dönün.
  • Ağır işi kuyruğa alın, önce cevap verin.
  • Kendi tarafınızdan dönen 4xx'i hata sayın: tekrar denemeleri durdurur.

Yapmayın

  • Cevap vermeden uzun iş çalıştırmayın, istek 10 saniyede zaman aşımına uğrar.
  • 3xx dönmeyin, yönlendirmeler takip edilmez.
  • Büyük yanıt gövdesi üretmeyin, gövde okunmadan atılır.
DenemeÖncesindeki beklemeactivation.code_receivedcancelled ve expired
1anında
210s
360s
45m
515m
630m

Ağ hatası, zaman aşımı, 408, 429 ve 5xx durumlarında tekrar denenir. Diğer 4xx yanıtları merdiveni anında durdurur. code_received 4 denemede kesilir, çünkü sonraki adımlar işe yarayacakken aktivasyon çoktan biter.

Tekilleştirme

X-SMSBulk-Event-Id değerini idempotency anahtarı olarak kullanın. Aynı olayın tekrarları aynı id ile gelir.

En az bir kez, tam olarak bir kez değil

Aktivasyon ve olay tipi başına tek kayıt açılır, bu yüzden kopya nadirdir. Yine de takılan iş kurtarması aynı kaydı yeniden teslim edebilir. Olay id'sini saklayın, daha önce işlediğiniz id'yi yok sayın.

Devre kesici

Ölü bir uç sonsuza kadar çağrılmaz.

  • 20 ardışık başarısız teslimattan sonra uç pasife alınır.
  • Size e-posta gider, panelde pasife alınma sebebi görünür.
  • Uç pasifken oluşan olaylar geri gönderilmez. Telafi için GET /v1/activations kullanın.

Secret yenileme

İmza secret'ını kesinti olmadan değiştirin.

  • Rotate ucu yeni secret'ı bir kez döner.
  • Geçiş penceresinde hem yeni hem önceki imza birlikte gönderilir.
  • Yeni secret'a kendi takviminizle geçin, pencere bitince yalnız yeni imza gönderilir.

Ucunuzu kurun

URL kaydedin, test olayı gönderin, teslimat listesini panelden izleyin.