Ana sayfa › SMS API › SMS Webhook
OTP webhook bildirimleri

İmza Doğrulamalı SMS Webhook'ları

SMS kodu geldiğinde Turonay sunucunuza imzalı bir HTTPS POST gönderir. Kodu dakikalarca yoklamanız gerekmez; gelen isteğin gerçekten bizden geldiğini ise HMAC-SHA256 imzasıyla doğrularsınız. Bu rehber olayları, istek gövdesini, imza algoritmasını, yeniden deneme kurallarını ve PHP, Python ile Node.js doğrulama kodlarını eksiksiz anlatır.

5 snyanıt süresi
8teslimat denemesi
HMAC-SHA256imza algoritması

Neden yoklama değil, webhook?

Kod bekleyen bir aktivasyonu getStatus ile birkaç saniyede bir sorgulamak işe yarar ama pahalıdır: yüzlerce eşzamanlı aktivasyonda istek sayınız hızla artar, hız sınırına yaklaşırsınız ve kodu yoklama aralığınız kadar geç görürsünüz. Webhook ters yönde çalışır: kod kaydedildiğinde bildirim kuyruğa alınır, genellikle 1–2 dakika içinde siz sormadan size ulaşır.

Üç somut faydası var: bildirim size kendiliğinden gelir (genellikle 1–2 dakika içinde), API çağrı sayınız (ve hız sınırı baskısı) düşer, sunucunuz boşuna beklemez. Karşılığında iki şeyi üstlenirsiniz: dışarıdan gelen isteğe güvenmeden önce imzayı doğrulamak ve aynı olayın tekrar gelebileceğini hesaba katmak. Aşağıdaki bölümler tam olarak bunu anlatır.

Olaylar

Her istek hangi olay olduğunu event alanında söyler; aynı değer X-Turonay-Event başlığında da bulunur.

OlayNe zaman gönderilir
code_receivedAktivasyon için SMS kodu kaydedildiğinde. Ücret bu anda kesinleşir; code ve text alanları doludur.
cancelledKod gelmeden aktivasyon iptal edildiğinde: sağlayıcı numarayı iptal ettiğinde ya da siz setStatus ile status=8 gönderdiğinizde. Bloke tutar serbest kalır, ücret alınmaz. code, text ve receivedAt alanları null olur.
testProfilinizde “Test gönder”e bastığınızda. Örnek değerler taşır, gerçek bir aktivasyona bağlı değildir.
Sessiz süre aşımı için cancelled gönderilmez. Kimse iptal etmeden süresi dolan bir aktivasyon için bildirim gelmez; bunları getStatus ya da getActiveActivations ile kendiniz izleyin (getNumberV2 yanıtındaki activationEndTime bitiş zamanını verir).

Aynı aktivasyona sonradan yeni bir kod gelirse (ek SMS), bu farklı bir id ile ayrı bir code_received olayı olarak iletilir.

İstek gövdesi (payload)

Gövde UTF-8 JSON’dur ve Content-Type: application/json ile gelir. Kod geldiğinde şuna benzer bir istek alırsınız:

http
POST /turonay-webhook HTTP/1.1
Host: api.example.com
Content-Type: application/json
User-Agent: Turonay-Webhook/1.0
X-Turonay-Event: code_received
X-Turonay-Timestamp: 1791630193
X-Turonay-Signature: sha256=3f1c…9ab2
X-Turonay-Delivery: dlv_9f2c4a7d1b3e5f60a8c2d4e6
json
{
  "event": "code_received",
  "activationId": "184213",
  "phone": "905301234567",
  "service": "wa",
  "country": "62",
  "code": "123456",
  "text": "Your WhatsApp code: 123-456",
  "receivedAt": "2026-10-10T11:03:12Z",
  "sentAt": "2026-10-10T11:03:13Z",
  "id": "dlv_9f2c4a7d1b3e5f60a8c2d4e6"
}

İptal olayında aynı alanlar bulunur; yalnızca code, text ve receivedAt değerleri null olur:

json
{
  "event": "cancelled",
  "activationId": "184214",
  "phone": "6281234567890",
  "service": "tg",
  "country": "6",
  "code": null,
  "text": null,
  "receivedAt": null,
  "sentAt": "2026-10-10T11:08:40Z",
  "id": "dlv_3b7e1c9a5d2f4860b1c3e5a7"
}
AlanTürAçıklama
eventstringOlay adı: code_received, cancelled veya test.
activationIdstringAktivasyon kimliği; getNumber yanıtındaki kimlikle aynıdır.
phonestringAktivasyondaki telefon numarası (ülke kodu dahil).
servicestringServis kodu (örneğin wa).
countrystringÜlke kimliği (örneğin 62).
codestring | nullSMS kodu; cancelled olayında null.
textstring | nullOperatörden gelen SMS metni; boş olabilir, cancelled olayında null.
receivedAtstring | nullKodun kaydedildiği an, UTC olarak ISO 8601 biçiminde (ör. 2026-10-10T11:03:12Z). cancelled olayında null.
sentAtstringBu teslimatın gönderildiği an, UTC, ISO 8601. Kesin zaman için bunu kullanın.
idstringTeslimat kimliği (dlv_…); yeniden denemelerde değişmez.

İstek başlıkları

Her istekte aşağıdaki başlıklar bulunur. İmza doğrulaması için yalnızca zaman damgası ve imza başlığı gerekir; teslimat kimliği tekilleştirme içindir.

BaşlıkAçıklama
Content-TypeHer zaman application/json.
User-Agentİstemci adı: Turonay-Webhook/1.0.
X-Turonay-EventOlay adı (code_received, cancelled, test).
X-Turonay-TimestampGönderim anı, Unix saniyesi. İmzanın bir parçasıdır.
X-Turonay-Signaturesha256= + onaltılık HMAC-SHA256 imzası.
X-Turonay-DeliveryTeslimat kimliği; gövdedeki id ile aynıdır ve yeniden denemelerde değişmez.

İmza nasıl doğrulanır?

İmza, gizli anahtarınızla (whsec_ ile başlar) üretilen bir HMAC-SHA256 değeridir. Doğrulama altı adımdır:

  1. Gövdeyi ham bayt olarak okuyun. JSON’u ayrıştırıp yeniden serileştirmeyin; boşluk ya da anahtar sırası değişirse imza tutmaz.
  2. X-Turonay-Timestamp ve X-Turonay-Signature başlıklarını alın.
  3. Zaman damgası şu andan 5 dakikadan fazla uzaktaysa isteği reddedin (tekrar saldırısı koruması).
  4. Şu dizgeyi oluşturun: zaman damgası + . + ham gövde.
  5. Bu dizgenin gizli anahtarla HMAC-SHA256 değerini onaltılık olarak hesaplayın ve başına sha256= ekleyin.
  6. Sonucu başlıktaki imzayla sabit zamanlı (timing-safe) bir karşılaştırmayla kıyaslayın. Eşleşmiyorsa 401 döndürün.
text
signature = "sha256=" + hex( HMAC_SHA256( secret, timestamp + "." + rawBody ) )

Gizli anahtar adresi ilk kaydettiğinizde otomatik üretilir ve tam hâliyle yalnızca o anda gösterilir; sonra maskelenir. Sızdığından şüphelenirseniz yeni bir anahtar üretin: eskisi anında geçersiz olur. Anahtarı kaynak koda değil, ortam değişkenine koyun.

Doğrulama kodu: PHP, Python, Node.js

Üç örnek de aynı kuralları uygular: ham gövde, 5 dakikalık tolerans, sabit zamanlı karşılaştırma ve hızlı 2xx yanıtı. Ağır işi (veritabanı, ikinci bir API çağrısı) yanıttan sonra kuyrukta yapın; yanıt süreniz 5 saniyeyi aşmamalı.

PHP

php
<?php
// webhook.php — receives Turonay SMS webhooks
$secret = getenv('TURONAY_WEBHOOK_SECRET');            // whsec_...
$raw    = file_get_contents('php://input');             // RAW bytes: never json_decode() before verifying
$ts     = $_SERVER['HTTP_X_TURONAY_TIMESTAMP'] ?? '';
$sig    = $_SERVER['HTTP_X_TURONAY_SIGNATURE'] ?? '';

// 1) replay protection: reject anything more than 5 minutes away from now
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }

// 2) signature = "sha256=" + HMAC_SHA256(secret, timestamp + "." + rawBody)
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) { http_response_code(401); exit; }   // constant-time compare

// 3) de-duplicate on the delivery id (identical across all retries)
$event = json_decode($raw, true);
$stmt  = $pdo->prepare('INSERT IGNORE INTO webhook_deliveries (id) VALUES (?)');   // $pdo = your PDO connection
$stmt->execute([$event['id']]);
if ($stmt->rowCount() === 0) { http_response_code(200); exit; }          // already processed

if ($event['event'] === 'code_received') {
    // $event['activationId'], $event['code'], $event['text'] ...
}
http_response_code(200);                                // answer within 5 seconds

Python (Flask)

python
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["TURONAY_WEBHOOK_SECRET"].encode()   # whsec_...
seen = set()                                              # use Redis / a unique DB column in production


@app.post("/turonay-webhook")
def turonay_webhook():
    raw = request.get_data()                              # RAW bytes
    ts = request.headers.get("X-Turonay-Timestamp", "")
    sig = request.headers.get("X-Turonay-Signature", "")

    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        abort(400)                                        # replay protection (5 min)

    expected = "sha256=" + hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected.encode(), sig.encode()):   # constant-time compare
        abort(401)

    event = request.get_json(force=True)
    if event["id"] in seen:                               # idempotency: same id on every retry
        return "", 200
    seen.add(event["id"])

    if event["event"] == "code_received":
        print(event["activationId"], event["code"])
    return "", 200                                        # answer within 5 seconds

Node.js (Express)

js
const crypto = require("crypto");
const express = require("express");

const app = express();
const SECRET = process.env.TURONAY_WEBHOOK_SECRET;       // whsec_...
const seen = new Set();                                   // use Redis / a unique DB column in production

// express.raw keeps the body as a Buffer: the signature covers the exact bytes
app.post("/turonay-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const ts = req.get("X-Turonay-Timestamp") || "";
  const sig = req.get("X-Turonay-Signature") || "";

  if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return res.sendStatus(400);                           // replay protection (5 min)
  }

  const expected = "sha256=" + crypto.createHmac("sha256", SECRET)
    .update(ts + ".").update(req.body).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(sig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);                           // constant-time compare
  }

  const event = JSON.parse(req.body.toString("utf8"));
  if (seen.has(event.id)) return res.sendStatus(200);     // idempotency: same id on every retry
  seen.add(event.id);

  if (event.event === "code_received") console.log(event.activationId, event.code);
  res.sendStatus(200);                                    // answer within 5 seconds
});

app.listen(3000);

Yeniden deneme politikası

Sunucunuz 5 saniye içinde bir 2xx durum koduyla yanıt vermelidir. Başka bir durum kodu, zaman aşımı ya da bağlantı hatası başarısızlık sayılır; yanıt gövdesi okunmaz. Yönlendirmeler (3xx) takip edilmez ve başarısızlık sayılır.

Başarısız teslimat en fazla 8 kez denenir. Bekleme süresi 1 dakikadan başlar ve her denemede iki katına çıkar, üst sınırı 1 saattir (1, 2, 4, 8, 16, 32, 60 dakika; toplam yaklaşık iki saat). Tüm denemeler başarısız olursa olay düşürülür. Adresin geçersiz bulunması gibi kalıcı hatalar yeniden denenmez.

Her deneme, profilinizdeki “son 10 teslimat” listesine HTTP durumu ve hata notuyla yazılır; bir şey ters gittiğinde ilk bakacağınız yer orasıdır.

Tekrarlanan olaylar ve idempotency

Teslimat “en az bir kez” ilkesiyle çalışır: yanıtınız zamanında ulaşmazsa aynı olay yeniden gelir. Her teslimatın id alanı (ve X-Turonay-Delivery başlığı) tüm denemelerde aynıdır; bu yüzden tekilleştirme için ideal anahtardır.

Pratik kural: id değerini benzersiz bir sütunda saklayın; kayıt zaten varsa işlemi atlayın ama yine de 200 döndürün. Olayların sırası garanti edilmez, farklı aktivasyonların olayları iç içe gelebilir.

Kodu getStatus ile de okuyorsanız activationId bazında da tekilleştirin: aynı kod hem yoklamayla hem webhook ile elinize ulaşabilir.

Webhook adresi için güvenlik kuralları

Webhook adresiniz sunucumuzdan istek attırdığı için, sunucularımızın iç ağlara yönlendirilmesini önleyen sıkı kurallar uygularız. Kurallar adresi kaydederken ve her teslimattan önce yeniden denetlenir. Adresiniz şunları sağlamalıdır:

  • Yalnızca https://. Düz http:// kabul edilmez ve TLS sertifikası geçerli olmalıdır.
  • Yalnızca 443 ya da 8443 portu. Port yazmazsanız 443 kullanılır.
  • En fazla 500 karakter; kullanıcı adı ve parola (kullanici:parola@) içeremez.
  • Ana makine bir alan adı olmalıdır. IP adresleri (ondalık, sekizlik ve onaltılık yazımlar dahil), localhost, tek etiketli adlar ve .local, .internal, .test gibi iç ağ uzantıları reddedilir; alan adı noktayla bitmemelidir.
  • Alan adının çözümlendiği tüm IP adresleri genel (public) olmalıdır. Özel ağ, loopback, link-local, CGNAT, multicast, ayrılmış ve dokümantasyon aralıkları ile sunucularımızın kendi adresleri reddedilir.
  • Turonay’ın kendi alan adı reddedilir; kendi API’mize döngü oluşmasın diye.
  • Doğruladığımız IP’ye sabitlenerek bağlanırız (DNS rebinding’e karşı) ve yönlendirmeleri takip etmeyiz. Adresiniz yönlendirme döndürüyorsa son adresi doğrudan kaydedin.
Örnek adresSonuç
https://api.example.com/turonay-webhookGeçerli
http://api.example.com/hookReddedilir: https değil
https://203.0.113.10/hookReddedilir: IP adresi, alan adı değil
https://api.example.com:8080/hookReddedilir: izin verilmeyen port
https://user:pass@api.example.com/hookReddedilir: kullanıcı bilgisi içeriyor
https://localhost/hookReddedilir: iç ağ alan adı

Profilde webhook’u etkinleştirme

Webhook, kod yazmadan önce profilinizden ayarlanır:

  1. Giriş yaptıktan sonra Profil → API → Webhook bölümüne gidin ve Webhook kutusunu açın.
  2. Adresinizi (örneğin https://api.siteniz.com/turonay-webhook) yazıp kaydedin. İlk kayıtta gizli anahtar (whsec_…) otomatik üretilir; hemen kopyalayın, sonra yalnızca maskeli görünür.
  3. Test gönder düğmesine basın. test olayı gerçek bir imzayla adresinize gider; sonuç HTTP durumu ve süreyle birlikte ekranda görünür. Testler arasında birkaç saniye beklemeniz gerekir.
  4. Son 10 teslimat tablosunda zaman, olay, aktivasyon, HTTP durumu, deneme sayısı ve hata notunu görürsünüz.
  5. Webhook’u kapatmak için alanı boş bırakıp kaydedin. Gizli anahtarı yenilemek için “Yeni anahtar üret” düğmesini kullanın; eski anahtar anında geçersiz olur.

Webhook + getStatus: en sağlam düzen

Webhook ana yol olsun, getStatus sigorta. Ağ kesintisi, dağıtım sırasında kapalı kalan sunucu ya da 8 denemenin de başarısız olması gibi durumlarda bir olayı kaçırabilirsiniz; bu yüzden bekleyen aktivasyonları hiç unutmayın.

  1. Numara aldığınızda aktivasyonu “bekliyor” olarak kaydedin.
  2. Webhook gelince önce id ile tekilleştirin, kodu işleyin ve aktivasyonu kapatın.
  3. Birkaç dakikada bir hâlâ bekleyen aktivasyonlar için getStatus çağırın; STATUS_OK:kod dönerse aynı işleyiciye verin.
  4. Uygulama yeniden başladığında getActiveActivations ile açık aktivasyonları geri yükleyin.
python
import requests

API = "https://turonay.com/stubs/handler_api.php"


def sweep(waiting_ids):          # run every few minutes
    for act_id in waiting_ids:
        text = requests.get(API, params={
            "api_key": "YOUR_API_KEY", "action": "getStatus", "id": act_id,
        }, timeout=15).text
        if text.startswith("STATUS_OK:"):
            handle_code(act_id, text.split(":", 1)[1])   # the same handler the webhook uses

Böylece webhook bildirimi size iletir (genellikle 1–2 dakika içinde), yoklama da nadir kayıpları kapatır; yoklama sıklığını düşük tutabildiğiniz için API çağrı sayınız da düşük kalır. Dile özel örnekler için Python, PHP ve Node.js rehberlerine, tüm uç noktalar için API dokümanı sayfasına bakın.

Sık sorulan sorular

SMS API’de webhook nedir, yoklamadan farkı ne?

Webhook, kod kaydedildiğinde Turonay’ın sunucunuza göndermek üzere kuyruğa aldığı imzalı bir HTTPS POST isteğidir; genellikle 1–2 dakika içinde ulaşır. Yoklamada kodu görmek için sürekli getStatus çağırırsınız; webhook’ta ise olay size kendiliğinden gelir. Bu, API çağrı sayısını azaltır.

Hangi olaylar gönderilir?

code_received (SMS kodu kaydedildi) ve cancelled (kod gelmeden iptal edildi) olayları gönderilir; profildeki test düğmesi ayrıca bir test olayı yollar. Kimse iptal etmeden süresi dolan aktivasyonlar için cancelled bildirimi gönderilmez; onları getStatus ile izleyin.

İmza nasıl doğrulanır?

X-Turonay-Timestamp değeri, bir nokta ve ham istek gövdesi birleştirilir; bu dizgenin gizli anahtarla HMAC-SHA256 değeri onaltılık olarak hesaplanır ve başına sha256= eklenir. Sonuç, X-Turonay-Signature başlığıyla sabit zamanlı bir karşılaştırmayla kıyaslanır. Zaman damgası 5 dakikadan eskiyse istek reddedilmelidir.

Yanıtım geç kalırsa ne olur?

Sunucunuz 5 saniye içinde 2xx dönmezse teslimat başarısız sayılır ve en fazla 8 denemeye kadar, 1 dakikadan başlayıp ikiye katlanan (en çok 1 saat) aralıklarla yeniden denenir. Aynı olay aynı id ile gelir; id’ye göre tekilleştirme yapın.

Webhook adresim neden reddediliyor?

Adres https olmalı, 443 ya da 8443 portunu kullanmalı, kullanıcı bilgisi içermemeli ve herkese açık IP’lere çözülen bir alan adı olmalıdır. IP adresleri, localhost, iç ağ alan adları ve yönlendirme yapan adresler kabul edilmez. Yerel geliştirmede bir tünel hizmeti kullanabilirsiniz.

Webhook varken getStatus’a ihtiyacım var mı?

Önerilir. Webhook ana yol, getStatus ise kaçırılan olaylar ve yeniden başlatmalar için yedektir. Bekleyen aktivasyonları arada bir getStatus ile kontrol edin.

Gizli anahtar sızarsa ne yapmalıyım?

Profilinizdeki API bölümünden yeni bir gizli anahtar üretin. Eski anahtar anında geçersiz olur; sunucunuzdaki değeri hemen güncelleyin, aksi hâlde imza doğrulaması başarısız olur.

Webhook’u şimdi deneyin

Ücretsiz hesap açın, API anahtarınızı alın, adresinizi kaydedip “Test gönder”e basın. Kod gelmeyen aktivasyonlar için ücret ödemezsiniz.