Dokümantasyon
Tek API çağrısı ve tek webhook ile telefon numarası doğrula. SDK kurmana gerek yok.
- Base URL
- https://replyauth.com/api/v1
- Kimlik doğrulama
- Authorization: Bearer <API_KEY>
Nasıl çalışır
Klasik OTP’nin tersi: kodu kullanıcı almaz, kendisi gönderir.
- Backend’in bir doğrulama oluşturur ve yanıtta hazır WhatsApp linkini alır.
- Kullanıcı linke dokunur; WhatsApp mesaj hazır yazılmış halde açılır.
- Kullanıcı gönder’e basar, mesaj bize ulaşır.
- Kodu eşleştirir, doğrulamayı
validatedyapar vecallback_urladresine imzalı bir POST atarız. - Frontend sonucu SSE veya polling ile anında görür.
Kimlik doğrulama
Her istek Authorization header’ında bir API key taşır. API key bir app’e aittir, panelden oluşturulur ve yalnızca bir kez gösterilir; biz sadece hash’ini saklarız.
Authorization: Bearer ra_live_...API key sunucunda kalsın
Sandbox
Yeni app’ler sandbox modunda başlar; akışın tamamını ücretsiz test edebilirsin. Gerçek trafiğe hazır olduğunda app ayarlarından kapat.
- Sandbox doğrulamaları hiç ücretlendirilmez, bakiye sıfırken de çalışır.
- Her organizasyon günde en fazla 50 sandbox doğrulaması oluşturabilir. Sınır aşılınca API
429 sandbox_quota_exceededdöner. - Yanıtlar ve webhook’lar
"sandbox": truetaşır; test trafiğini böyle ayırt edersin.
Bağlama politikası
Hiçbir şeye bağlanmamış bir doğrulama yalnızca bir telefondan mesaj gönderildiğini kanıtlar; hesabın kime ait olduğunu söylemez. Bu yüzden her app ayarlarında bir politika seçer.
strictvarsayılanauthorized_numbersister. Login, 2FA ve işlem onayı için: numarayı zaten biliyorsun, WhatsApp sahipliğini kanıtlar.subjectsubjectveyaauthorized_numbersister. Kayıt akışı için: numarayı henüz bilmiyorsun ama kimliği (e-posta, user id) biliyorsun. Numara o kimliğe bağlanır.open- Hiçbir şey istemez. Anonim numara toplama için; korumayı bilerek kapatırsın, risk senindir.
subject modu neyi yakalar
Bir subject ilk kez doğrulandığında numara ona bağlanır. Aynı subject sonra başka bir numarayla gelirse:
- Kimlik-numara kilidi açıksa (varsayılan) doğrulama reddedilir ve
status: failedwebhook’u gider. - Kapalıysa doğrulama geçer ama webhook’ta
subject_phone_mismatchsinyali gelir; kararı sen verirsin.
Böylece saldırgan kurbanın e-postasıyla giriş başlatıp kendi numarasından doğrulayamaz.
subject’i tek bir standart biçimde gönder
subject birebir, büyük-küçük harf dahil karşılaştırılır: [email protected] ile [email protected] iki farklı kimliktir. Küçük harfe çevrilmiş e-posta ya da kendi user id’n gibi sabit bir değer gönder.Risk sinyalleri
Her webhook geçerli sinyalleri risk_signals alanında listeler.
phone_new_for_subjectbilgi- Bu subject bu numarayla ilk kez doğrulanıyor.
subject_phone_mismatchyüksek- Subject daha önce başka bir numarayla doğrulanmış. Kilit açıksa doğrulama zaten reddedilir.
phone_shared_across_subjectsorta- Aynı numara eşiğini aşan sayıda farklı kimlikte kullanılıyor. Toplu hesap açma göstergesi.
Doğrulama oluşturma
GET/api/v1/verification_code
Bir kod üretir ve kullanıcıya göstereceğin WhatsApp linkini döner. JSON body ile POST da aynı alanlarla çalışır.
Parametreler
Tüm parametreler isteğe bağlı; hangi kimlik alanının zorunlu olduğuna app’in bağlama politikası karar verir.
expires_atinteger- Dakika cinsinden geçerlilik süresi, 1 ile 1440 arası. Verilmezse app ayarı kullanılır.
callback_urlstring- Sonucun POST edileceği HTTPS adres. Verilmezse app ayarı kullanılır.
authorized_numbersstring[] | csvstrict modda zorunlu- Doğrulamayı yalnızca bu numaralar tamamlayabilir, en fazla 10 numara. E.164 biçiminde, örneğin
+905321234567. subjectstringsubject modda zorunlu- Kullanıcı için senin tarafındaki benzersiz kimlik: e-posta, user id, sipariş no. Numara buna bağlanır. Birebir karşılaştırılır, en fazla 200 karakter.
contextstring- Kullanıcının WhatsApp mesajında okuyacağı “neyi onaylıyorum” satırı, en fazla 300 karakter. Relay’e karşı asıl savunman.
link_messagestring- Mesajda kodun altına eklenen yönerge, en fazla 700 karakter. Verilmezse app'in link mesajı, o da yoksa app'in mesaj dilindeki (Türkçe ya da İngilizce) hazır metin kullanılır.
qr0 | 1- Yanıta QR görseli (data URI) ekler.
channelwhatsapp- Şimdilik yalnızca
whatsapp;sms422 döner. metadataobject | json- Webhook’ta aynen geri döner. Session ID taşımak için birebir.
context mesajda kalmalı
context gönderdiysen gelen mesaj onu hâlâ içermeli. Büyük-küçük harf ve boşluk farkı yok sayılır. Kullanıcı metni silerse ya da değiştirirse kod harcanmaz ve doğrulama tamamlanmaz.Örnek
curl -G 'https://replyauth.com/api/v1/verification_code' \
--data-urlencode 'authorized_numbers=+905321234567' \
--data-urlencode '[email protected]' \
--data-urlencode 'context=Chrome / Windows üzerinden giriş · 14:32' \
--data-urlencode 'expires_at=2' \
--data-urlencode 'qr=1' \
--header 'Authorization: Bearer ra_live_...'{
"id": "6f9a1c2e-…",
"code": "K7M2P9XT4B",
"link": "https://wa.me/905102209823?text=%60K7M2P9XT4B%60…",
"deep_link": "whatsapp://send?phone=905102209823&text=…",
"qr": "data:image/png;base64,…",
"poll_token": "s0m3-r4nd0m-t0k3n",
"expires_at": "2026-08-03T12:41:00.000Z",
"wa_number": "+905102209823",
"sandbox": true
}poll_token tarayıcıya verilebilir; yalnızca bu doğrulamanın durumunu okur.
Durum sorgulama
Asıl kanal webhook. Bu endpoint’ler aradaki anlar için.
Doğrulamayı getir
GET/api/v1/verifications/{id}
Sunucu tarafında, API key ile çağrılır. Tam webhook payload’ını döner; webhook kaçarsa telafi yolun.
Polling ile durum
GET/api/v1/verifications/{id}/status
Tarayıcıdan, ?token= parametresine poll_token verilerek çağrılır. Yalnızca doğrulamanın başarılı olup olmadığını söyler; telefon numarası dönmez.
Canlı event akışı
GET/api/v1/verifications/{id}/events
Aynı ?token= ile Server-Sent Events. Son event’ten sonra stream kapanır.
open- Bağlanınca mevcut durumla gelir.
validated- Kullanıcının mesajı eşleşti. Son event.
closed- Doğrulama başarısız bitti;
statusdeğeriexpiredveyafailed. Son event. ping- Doğrulama beklerken bağlantıyı canlı tutar.
timeout- Stream süre sınırına ulaştı. Yeniden bağlan ya da polling’e geç.
Webhook
Doğrulama son durumuna ulaşınca callback_url adresine imzalı bir JSON POST atarız. requested için webhook gitmez; doğrulama sürerken SSE veya polling kullan.
{
"id": "6f9a1c2e-…",
"status": "validated",
"verification_code": "K7M2P9XT4B",
"phone_number": "+905321234567",
"profile_name": "Ada",
"channel": "whatsapp",
"authorized_numbers": ["+905321234567"],
"subject": "[email protected]",
"risk_signals": ["phone_new_for_subject"],
"metadata": { "session_id": "abc" },
"sandbox": false,
"requested_at": "2026-08-03T12:31:00.000Z",
"expires_at": "2026-08-03T12:41:00.000Z",
"validated_at": "2026-08-03T12:31:19.284Z",
"error": null
}status değeri validated, expired veya failed olur.
Header’lar
X-Replyauth-Signaturet=<timestamp>,v1=<hmac>: webhook secret’ınla<timestamp>.<raw body>üzerinden HMAC-SHA256. Secret’ı yeniledikten sonraki 24 saat boyunca başlıkta her secret için birv1bulunur (önce eski, sonra yeni): herhangi biri eşleşirse isteği kabul et. Sadece ilkv1’i okuyan kod yeni secret’a pencere bittikten sonra geçmeli. Pencere içinde tekrar yenilemek en eski secret’ı hemen düşürür; sızmış bir secret’ı anında geçersiz kılmanın yolu budur.X-Replyauth-Timestamp- Saniye cinsinden Unix zamanı;
tile aynı değer. X-Replyauth-Eventverification.validated,verification.expiredveyaverification.failed.X-Replyauth-Delivery- Teslimat ID’si. Retry’larda değişmez; tekrarlanan istekleri ayıklamak için kullan.
X-Replyauth-Attempt- Deneme numarası, 1 ile 6 arası.
Retry takvimi
10 saniye içinde dönen her 2xx yanıt teslim edildi sayılır. Diğer her şey şu takvimle tekrar denenir:
| Deneme | Gönderim |
|---|---|
| 1 | Hemen |
| 2 | 1. denemeden 30 saniye sonra |
| 3 | 2. denemeden 2 dakika sonra |
| 4 | 3. denemeden 10 dakika sonra |
| 5 | 4. denemeden 1 saat sonra |
| 6 | 5. denemeden 6 saat sonra |
Altısı da başarısız olursa teslimat abandoned işaretlenir ve hata panel log’larında görünür. Sonucu yine de Doğrulamayı getir ile alabilirsin.
İmzayı doğrulama
HMAC’i JSON’u parse etmeden önce, ham body üzerinden hesapla.
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req: Request) {
const raw = await req.text(); // raw body, not parsed JSON
const parts = (req.headers.get("x-replyauth-signature") ?? "").split(",");
const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2);
// During a secret rotation there is one v1 per active secret: accept any match
const received = parts.filter((p) => p.startsWith("v1=")).map((p) => Buffer.from(p.slice(3)));
// Reject signatures older than 5 minutes: replay protection
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return new Response("stale", { status: 400 });
}
const expected = Buffer.from(
createHmac("sha256", process.env.REPLYAUTH_WEBHOOK_SECRET!)
.update(`${timestamp}.${raw}`)
.digest("hex"),
);
if (!received.some((b) => b.length === expected.length && timingSafeEqual(b, expected))) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(raw);
if (event.status === "validated") {
if (event.risk_signals.includes("phone_shared_across_subjects")) {
await flagForReview(event.subject, event.phone_number);
}
await markSessionVerified(event.metadata.session_id, event.phone_number);
}
return new Response(null, { status: 204 }); // any 2xx
}Frontend
Önce kendi sunucunda bir proxy aç. API key orada kalır, tarayıcıya yalnızca görüntüleme için gereken alanlar iner:
// Runs on your server: the API key never reaches the browser.
export async function POST(req: Request) {
const session = await getSession();
const { phone } = await req.json();
const url = new URL("https://replyauth.com/api/v1/verification_code");
url.searchParams.set("expires_at", "2"); // short TTL narrows the relay window
url.searchParams.set("qr", "1");
url.searchParams.set("authorized_numbers", phone); // strict: only this number
url.searchParams.set("subject", session.user.id); // stable id, compared exactly
url.searchParams.set(
"context",
`Sign-in from ${session.device} · ${new Date().toLocaleString()}`,
);
url.searchParams.set("metadata", JSON.stringify({ session_id: session.id }));
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.REPLYAUTH_API_KEY}` },
});
const v = await res.json();
return Response.json({
id: v.id, link: v.link, deep_link: v.deep_link,
qr: v.qr, poll_token: v.poll_token, expires_at: v.expires_at,
});
}Sonra widget’ı ekle ve bu route’a bağla:
<div id="verify"></div>
<script src="https://replyauth.com/widget.js"></script>
<script>
Replyauth.mount("#verify", {
create: "/api/replyauth/start",
baseUrl: "https://replyauth.com",
onValidated: function () { location.href = "/welcome"; },
});
</script>Widget mobilde deep link butonu, masaüstünde QR kod gösterir. SSE ve polling’i birlikte çalıştırır; proxy stream’i kesse bile sonuç kaybolmaz. React kullanıyorsan bileşeni demo sayfasından kopyalayabilirsin.
Güvenlik
İki farklı saldırı
Numara sahteleme
EngellenirSaldırgan, kurbanın hesabı için başlatılan doğrulamayı kendi numarasından tamamlar.
authorized_numbers veya subject kilidi bunu tamamen engeller.
Relay
AzalırSaldırgan doğrulamayı başlatır ve wa.me linkini kurbana yollar; kurban gönderir.
Risk azalır ama sıfırlanmaz. Aşağıdaki önlemleri uygula.
Önemli: authorized_numbers relay’i tek başına durdurmaz. Saldırgan kurbanın numarasını zaten yazabilir; beklenen ve gelen numara aynı olur. Bu alan başka bir saldırıyı engeller. İkisi de gerçek, ikisini de kapat.
Relay’e karşı gerçekten işe yarayanlar
- context gönder. Kullanıcı gönder’e basmadan önce neyi onayladığını okur, örneğin “Chrome / Windows üzerinden giriş · 14:32”. Girişi kendisi başlatmayan kurbanı durduracak tek şey budur ve maliyeti yoktur.
- Kısa TTL kullan.
expires_at=2saldırganı gerçek zamanlı relay yapmaya zorlar. Pencereyi daraltır; relay’i tek başına durdurmaz. - Kimlik başına rate limit.
subjectgönderirsen denemeler 5 dakikada 5 ile sınırlanır. - Hassas işlemde ikinci faktör iste. Para transferi veya şifre değişikliği tek başına telefon doğrulamasına dayanmamalı. Bu her OTP yöntemi için geçerli.
- Oturum parmak izini taşı:
metadataile gönder, webhook’ta kendi kaydınla karşılaştır.
Platformda hazır gelenler
- 50 bit entropili, tek kullanımlık ve süreli kodlar.
- Zorunlu bağlama politikası; bir app kazayla open kalamaz.
- Kimlik-numara tutarlılık kontrolü ve risk sinyalleri.
- HMAC-SHA256 imzalı, timestamp’li webhook’lar.
callback_urliçin SSRF taraması: HTTPS zorunlu, özel IP’ye çözülen adresler reddedilir.- Tarayıcı erişimi origin allowlist ile sınırlı.
- Yetkisiz numaradan gelen mesaj kodu yakmaz; numarayı bilen biri doğrulamayı engelleyemez.
authorized_numbersE.164 olarak doğrulanır; hatalı numara kullanıcıyı sessizce kilitlemek yerine 422 döner.- API key, session token ve poll token yalnızca hash olarak saklanır.
Hata kodları
Hatalar 2xx dışı bir status ve sabit bir code içeren JSON body ile döner:
{ "error": { "code": "insufficient_balance", "message": "…" } }unauthorized401- API key eksik, hatalı veya iptal edilmiş.
insufficient_balance402- Doğrulama bakiyen bitti.
app_disabled403- App kapalı.
not_found404- Doğrulama bulunamadı.
invalid_request422- Bir parametre doğrulamadan geçmedi; ayrıntı
detailsalanında. invalid_callback_url422callback_urlHTTPS değil veya özel bir adrese çözülüyor.authorized_numbers_required422- App strict modda ve numara gönderilmedi.
subject_required422- App subject modda ve ne subject ne de numara gönderildi.
invalid_phone_number422- E.164 değil: ülke kodu eksik ya da numara 0 ile başlıyor.
channel_not_supported422channel=smsgönderildi; SMS henüz yok.rate_limited429- API key, organizasyon veya subject başına istek sınırı aşıldı. Ne zaman tekrar deneyeceğini
details.retry_after_secondssöyler. sandbox_quota_exceeded429- Organizasyon bugünkü 50 sandbox doğrulamasını kullandı.
internal_error500- Bizim tarafta beklenmeyen bir hata.
no_number_available503- Havuzda aktif WhatsApp numarası yok.
Self-hosting
Yalnızca kendi Replyauth kurulumunu çalıştırıyorsan gerekli. Meta kurulumunu ve numara paylaşım rehberini göster.
Meta kurulumu
- Meta for Developers’ta bir app aç ve WhatsApp ürününü ekle.
- Numaranı bağla,
phone_number_iddeğerini not al. - Uzun ömürlü bir System User token üret ve
META_ACCESS_TOKENolarak ayarla. API Setup’taki geçici token 24 saatte düşer ve production’ı her gün durdurur. - Webhook callback URL’ini
https://replyauth.com/api/webhooks/meta, verify token’ıMETA_VERIFY_TOKENyap; bu senin seçtiğin rastgele bir metin. - messages alanına abone ol ve WABA’nın app’e abone olduğunu doğrula (
/{WABA_ID}/subscribed_apps). Yalnızca callback URL tanımlamak mesaj getirmez. - Numarayı Panel → Numaralar ekranından havuza ekle. Hesabının
PLATFORM_ADMIN_EMAILSlistesinde olması gerekir.
Numarayı başka bir sistemle paylaşma
Meta’da webhook callback URL’i app seviyesindedir; bir WABA’nın tüm trafiği tek adrese gider. Numarayı n8n, destek aracı veya kendi bot’unla paylaşacaksan:
- Replyauth önde (önerilen):
INBOUND_FORWARD_URLdoğrulama kodu içermeyen her mesajı dokunmadan iletir.X-Hub-Signature-256korunur, karşı tarafın entegrasyonu çalışmaya devam eder. - Proxy arkasında: proxy JSON’u yeniden serialize ettiği için Meta’nın HMAC imzası korunamaz.
INBOUND_PROXY_SECRETayarla veX-Replyauth-Proxy-Secretheader’ı olarak gönder; imza yerine bu kontrol edilir.
Mesajı doğru app’e yönlendirme
Meta’nın inbound payload’ında tenant veya domain bilgisi yoktur; sadece numara, gönderen ve metin gelir. Kodu app’e bağlayan tek yer bizim veritabanımız. Proxy’nin dallanması gerekiyorsa:
curl -H "X-Replyauth-Proxy-Secret: $INBOUND_PROXY_SECRET" \
"https://replyauth.com/api/v1/resolve?body=%60K7M2P9XT4B%60"
{
"matched": true,
"status": "requested",
"subject": "[email protected]",
"app": { "id": "…", "name": "Web login" },
"org": { "id": "…", "name": "Acme" },
"domain": "acme.com",
"callback_host": "api.acme.com"
}code veya body alır. Telefon numarası, profil adı, poll token ve webhook secret bilerek dönmez; routing için gerekmezler. Endpoint yalnızca INBOUND_PROXY_SECRET ayarlıysa açıktır.
Proxy’nin “bu mesaj bizim mi” sorusunu cevaplamak için lookup’a ihtiyacı yok; büyük-küçük harfe duyarsız bir regex yeter: `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{10}`. Kod alfabesinde I, L, O ve U yoktur.
Paylaşılan numarada inbound_messages tablosu gövdeyi yalnızca doğrulama kodu içeren mesajlar için saklar; diğerlerinden idempotency için sadece mesaj ID’si tutulur.