İçeriğe geç

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.

  1. Backend’in bir doğrulama oluşturur ve yanıtta hazır WhatsApp linkini alır.
  2. Kullanıcı linke dokunur; WhatsApp mesaj hazır yazılmış halde açılır.
  3. Kullanıcı gönder’e basar, mesaj bize ulaşır.
  4. Kodu eşleştirir, doğrulamayı validated yapar ve callback_url adresine imzalı bir POST atarız.
  5. 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.

http
Authorization: Bearer ra_live_...

API key sunucunda kalsın

API key asla tarayıcıya inmemeli. Frontend doğrulama başlatacaksa kendi sunucunda küçük bir proxy endpoint aç. Örneği Frontend bölümünde.

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_exceeded döner.
  • Yanıtlar ve webhook’lar "sandbox": true taşı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ılan
authorized_numbers ister. Login, 2FA ve işlem onayı için: numarayı zaten biliyorsun, WhatsApp sahipliğini kanıtlar.
subject
subject veya authorized_numbers ister. 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: failed webhook’u gider.
  • Kapalıysa doğrulama geçer ama webhook’ta subject_phone_mismatch sinyali 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; sms 422 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

bash
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_...'
200 OK
{
  "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; status değeri expired veya failed. 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ç.
Tarayıcıdan çağrılan iki endpoint için app ayarlarındaki izinli origin’ler listesi dolu olmalı; yoksa CORS reddeder.

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.

POST {callback_url}
{
  "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-Signature
t=<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 bir v1 bulunur (önce eski, sonra yeni): herhangi biri eşleşirse isteği kabul et. Sadece ilk v1’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ı; t ile aynı değer.
X-Replyauth-Event
verification.validated, verification.expired veya verification.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:

DenemeGönderim
1Hemen
21. denemeden 30 saniye sonra
32. denemeden 2 dakika sonra
43. denemeden 10 dakika sonra
54. denemeden 1 saat sonra
65. 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.

app/api/replyauth/webhook/route.ts
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:

app/api/replyauth/start/route.ts
// 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:

html
<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

Engellenir

Saldı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ır

Saldı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

  1. 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.
  2. Kısa TTL kullan. expires_at=2 saldırganı gerçek zamanlı relay yapmaya zorlar. Pencereyi daraltır; relay’i tek başına durdurmaz.
  3. Kimlik başına rate limit. subject gönderirsen denemeler 5 dakikada 5 ile sınırlanır.
  4. 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.
  5. Oturum parmak izini taşı: metadata ile gönder, webhook’ta kendi kaydınla karşılaştır.
Relay SMS OTP’de de var (“kodu bana söyle” dolandırıcılığı) ve orada çok daha kolay. Burada saldırgan, kurbanı neyin onaylandığını yazan bir ekranda gönder’e basmaya ikna etmek zorunda. Daha iyi ama sıfır değil; aksini iddia etmiyoruz.

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_url iç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_numbers E.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:

json
{ "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ı details alanında.
invalid_callback_url422
callback_url HTTPS 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_supported422
channel=sms gö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_seconds sö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

  1. Meta for Developers’ta bir app aç ve WhatsApp ürününü ekle.
  2. Numaranı bağla, phone_number_id değerini not al.
  3. Uzun ömürlü bir System User token üret ve META_ACCESS_TOKEN olarak ayarla. API Setup’taki geçici token 24 saatte düşer ve production’ı her gün durdurur.
  4. Webhook callback URL’ini https://replyauth.com/api/webhooks/meta, verify token’ı META_VERIFY_TOKEN yap; bu senin seçtiğin rastgele bir metin.
  5. 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.
  6. Numarayı Panel → Numaralar ekranından havuza ekle. Hesabının PLATFORM_ADMIN_EMAILS listesinde 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_URL doğrulama kodu içermeyen her mesajı dokunmadan iletir. X-Hub-Signature-256 korunur, 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_SECRET ayarla ve X-Replyauth-Proxy-Secret header’ı 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:

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