İ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.
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.
| Olay | Ne zaman gönderilir |
|---|---|
code_received | Aktivasyon için SMS kodu kaydedildiğinde. Ücret bu anda kesinleşir; code ve text alanları doludur. |
cancelled | Kod 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. |
test | Profilinizde “Test gönder”e bastığınızda. Örnek değerler taşır, gerçek bir aktivasyona bağlı değildir. |
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:
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{
"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:
{
"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"
}| Alan | Tür | Açıklama |
|---|---|---|
event | string | Olay adı: code_received, cancelled veya test. |
activationId | string | Aktivasyon kimliği; getNumber yanıtındaki kimlikle aynıdır. |
phone | string | Aktivasyondaki telefon numarası (ülke kodu dahil). |
service | string | Servis kodu (örneğin wa). |
country | string | Ülke kimliği (örneğin 62). |
code | string | null | SMS kodu; cancelled olayında null. |
text | string | null | Operatörden gelen SMS metni; boş olabilir, cancelled olayında null. |
receivedAt | string | null | Kodun kaydedildiği an, UTC olarak ISO 8601 biçiminde (ör. 2026-10-10T11:03:12Z). cancelled olayında null. |
sentAt | string | Bu teslimatın gönderildiği an, UTC, ISO 8601. Kesin zaman için bunu kullanın. |
id | string | Teslimat 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ık | Açıklama |
|---|---|
Content-Type | Her zaman application/json. |
User-Agent | İstemci adı: Turonay-Webhook/1.0. |
X-Turonay-Event | Olay adı (code_received, cancelled, test). |
X-Turonay-Timestamp | Gönderim anı, Unix saniyesi. İmzanın bir parçasıdır. |
X-Turonay-Signature | sha256= + onaltılık HMAC-SHA256 imzası. |
X-Turonay-Delivery | Teslimat 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:
- 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.
X-Turonay-TimestampveX-Turonay-Signaturebaşlıklarını alın.- Zaman damgası şu andan 5 dakikadan fazla uzaktaysa isteği reddedin (tekrar saldırısı koruması).
- Şu dizgeyi oluşturun: zaman damgası +
.+ ham gövde. - Bu dizgenin gizli anahtarla HMAC-SHA256 değerini onaltılık olarak hesaplayın ve başına
sha256=ekleyin. - 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.
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
// 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 secondsPython (Flask)
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 secondsNode.js (Express)
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üzhttp://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,.testgibi 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 adres | Sonuç |
|---|---|
https://api.example.com/turonay-webhook | Geçerli |
http://api.example.com/hook | Reddedilir: https değil |
https://203.0.113.10/hook | Reddedilir: IP adresi, alan adı değil |
https://api.example.com:8080/hook | Reddedilir: izin verilmeyen port |
https://user:pass@api.example.com/hook | Reddedilir: kullanıcı bilgisi içeriyor |
https://localhost/hook | Reddedilir: iç ağ alan adı |
Profilde webhook’u etkinleştirme
Webhook, kod yazmadan önce profilinizden ayarlanır:
- Giriş yaptıktan sonra Profil → API → Webhook bölümüne gidin ve Webhook kutusunu açın.
- 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. - Test gönder düğmesine basın.
testolayı 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. - Son 10 teslimat tablosunda zaman, olay, aktivasyon, HTTP durumu, deneme sayısı ve hata notunu görürsünüz.
- 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.
- Numara aldığınızda aktivasyonu “bekliyor” olarak kaydedin.
- Webhook gelince önce
idile tekilleştirin, kodu işleyin ve aktivasyonu kapatın. - Birkaç dakikada bir hâlâ bekleyen aktivasyonlar için
getStatusçağırın;STATUS_OK:koddönerse aynı işleyiciye verin. - Uygulama yeniden başladığında
getActiveActivationsile açık aktivasyonları geri yükleyin.
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 usesBö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.