Node.js ile SMS Kodu Alma
Node.js 18 ve sonrasında yerleşik gelen fetch ile Turonay SMS onay API'sini kullanmanın eksiksiz bir örneği burada: numara satın alın, SMS kodunu yoklayın, aktivasyonu tamamlayın ya da iptal edin. Turonay handler_api protokolüyle çalıştığından bu kod, aynı protokolü kullanan bot ve kütüphanelerle de uyumludur.
Hiçbir HTTP paketi gerekmez; yalnızca webhook alıcısı için Express kuruyoruz. Sayfa bakiye sorgusuyla başlar, ardından async/await ile yazılmış tam akışı, JSON uçlarını, ikinci SMS isteğini, imza doğrulamalı webhook'u ve hata tablosunu anlatır. Genel bakış için SMS API, parametre ayrıntıları için API dokümanı sayfasına bakın.
Ön koşullar
Başlamadan önce üç şey hazır olmalı:
- API anahtarı: kayıt olduktan sonra profilinizdeki API bölümü içinde bulunur. Anahtar bakiyenizi harcatabilir; bu yüzden yalnızca sunucu tarafındaki Node.js kodunda kullanın,
process.envüzerinden okuyun ve tarayıcıya göndermeyin. - Bakiye:
getNumberfiyatı rezerve eder; tutarlar Türk lirası (TRY) cinsindendir. - Node.js 18 veya üstü:
fetchveAbortSignal.timeoutyerleşik olarak gelir.
EARLY_CANCEL_DENIED ile reddedilir.Kurulum: bağımlılık yok
Sürümü kontrol edin ve anahtarı ortam değişkenine yazın. Yalnızca webhook alıcısını çalıştıracaksanız express kurulur; API çağrıları için paket gerekmez. Örnekler CommonJS ile yazıldığından ek bir yapılandırma da istemez.
node --version # v18 or newer: fetch() is built in, no HTTP package needed
npm install express # only needed for the webhook receiver below
export TURONAY_API_KEY="paste-your-api-key-here"İlk istek: bakiyeyi okuyun
En kısa çalışan örnek getBalance isteğidir. API GET ile çalışır ve yanıt JSON değil düz metindir; başarıda ACCESS_BALANCE:125.4000 gelir, yanlış anahtarda BAD_KEY. res.json() çağırmayın: önce res.text() ile okuyup trim() yapın.
// balance.js - run with: node balance.js
const API = "https://turonay.com/stubs/handler_api.php";
async function main() {
const query = new URLSearchParams({
api_key: process.env.TURONAY_API_KEY,
action: "getBalance",
});
const res = await fetch(`${API}?${query}`, { signal: AbortSignal.timeout(20_000) });
const text = (await res.text()).trim(); // plain text, not JSON: "ACCESS_BALANCE:125.4000"
if (text.startsWith("ACCESS_BALANCE:")) {
console.log("Balance (TRY):", text.split(":")[1]);
} else {
console.log("Error:", text); // e.g. BAD_KEY
}
}
main();Node.js ile tam akış: numara al, kodu bekle, kapat
Aşağıdaki betik bütün yaşam döngüsünü async/await ile yürütür. getNumber() fiyat tavanıyla (maxPrice) alım yapar, waitForCode() her 5 saniyede getStatus sorar, en çok 20 dakika bekler ve sonuca göre finish() ya da cancel() çalışır. Fonksiyon adları Turonay'ın Node referans istemcisiyle aynıdır. node sms-flow.js ile çalıştırın.
// sms-flow.js - buy a number, wait for the SMS code, finish or cancel. Node 18+, no packages.
const API = "https://turonay.com/stubs/handler_api.php";
const API_KEY = process.env.TURONAY_API_KEY;
// The API answered with a plain-text error code (NO_BALANCE, NO_NUMBERS, ...).
class ApiError extends Error {}
// Network trouble, HTTP 429 or 5xx, NO_CONNECTION: safe to retry later.
class Transient extends ApiError {}
class CodeTimeout extends Error {}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(action, params = {}) {
const query = new URLSearchParams({ api_key: API_KEY, action });
for (const [key, value] of Object.entries(params)) {
if (value != null) query.set(key, String(value));
}
let res;
try {
res = await fetch(`${API}?${query}`, { signal: AbortSignal.timeout(20_000) });
} catch (err) {
throw new Transient(`network error: ${err.name}`); // never log the URL: it holds your api_key
}
if (res.status !== 200) throw new Transient(`HTTP ${res.status}`);
const text = (await res.text()).trim(); // always trim: bodies are plain text
if (text === "NO_CONNECTION") throw new Transient(text);
return text;
}
async function getBalance() {
const text = await call("getBalance");
if (!text.startsWith("ACCESS_BALANCE:")) throw new ApiError(text);
return text.split(":")[1];
}
// Returns [activationId, phone]. The price is only RESERVED here, not charged.
async function getNumber(service, country, maxPrice = null) {
const text = await call("getNumber", {
service,
country,
maxPrice: maxPrice == null ? null : maxPrice.toFixed(2),
});
const [tag, id, phone] = text.split(":");
if (tag === "ACCESS_NUMBER" && id && phone) return [id, phone];
throw new ApiError(text); // NO_BALANCE, NO_NUMBERS, BAD_SERVICE, WRONG_MAX_PRICE:<price> ...
}
// Returns ["wait" | "ok" | "cancel", code | null].
async function getStatus(id) {
const text = await call("getStatus", { id });
if (text.startsWith("STATUS_OK:")) return ["ok", text.slice("STATUS_OK:".length)];
if (text === "STATUS_CANCEL") return ["cancel", null];
if (text === "STATUS_WAIT_CODE" || text === "STATUS_WAIT_RESEND" || text.startsWith("STATUS_WAIT_RETRY:")) {
return ["wait", null]; // WAIT_RETRY carries the OLD code: still waiting for the new one
}
throw new ApiError(text); // NO_ACTIVATION, BAD_KEY ...
}
const setStatus = (id, status) => call("setStatus", { id, status });
const requestAnotherSms = (id) => setStatus(id, 3); // free, ACCESS_RETRY_GET
const finish = (id) => setStatus(id, 6); // ACCESS_ACTIVATION, only after a code
const cancel = (id) => setStatus(id, 8); // ACCESS_CANCEL / EARLY_CANCEL_DENIED
// Poll every `interval` seconds for at most `timeout` seconds (20 minutes).
async function waitForCode(id, { timeout = 1200, interval = 5 } = {}) {
const deadline = Date.now() + timeout * 1000;
let delay = interval;
while (Date.now() < deadline) {
try {
const [state, code] = await getStatus(id);
delay = interval;
if (state === "ok") return code;
if (state === "cancel") throw new ApiError("STATUS_CANCEL");
} catch (err) {
if (!(err instanceof Transient)) throw err;
delay = Math.min(delay * 2, 60); // back off on HTTP 429 / network trouble
}
await sleep(Math.max(0, Math.min(delay * 1000, deadline - Date.now())));
}
throw new CodeTimeout(`no SMS within ${timeout} seconds`);
}
async function main() {
const [service, country] = ["wa", "62"]; // WhatsApp, Turkey (see getPrices / getCountries)
let id;
let phone;
try {
console.log("Balance (TRY):", await getBalance());
[id, phone] = await getNumber(service, country, 15.0);
} catch (err) {
if (!(err instanceof ApiError)) throw err;
if (err instanceof Transient) console.error("Temporary problem, try again later:", err.message);
else if (err.message.startsWith("WRONG_MAX_PRICE")) console.error("Current price is above your limit:", err.message.split(":")[1] ?? "?");
else if (err.message === "NO_NUMBERS") console.error("No numbers in stock right now, try again in a minute");
else if (err.message === "NO_BALANCE") console.error("Not enough balance, top up first");
else console.error("API error:", err.message);
return 1;
}
console.log(`Use this number: ${phone} (activation ${id})`);
let code;
try {
code = await waitForCode(id);
} catch (err) {
if (err instanceof CodeTimeout) {
const reply = await cancel(id); // nothing is charged when no code arrived
if (reply !== "EARLY_CANCEL_DENIED") {
console.error("Timed out, activation cancelled:", reply);
return 1;
}
code = (await getStatus(id))[1]; // the code arrived in the last second
} else if (err instanceof ApiError) {
console.error("Activation cancelled:", err.message); // STATUS_CANCEL: nothing charged
return 1;
} else {
throw err;
}
}
console.log("SMS code:", code);
console.log(await finish(id)); // ACCESS_ACTIVATION
return 0;
}
main().then((status) => { process.exitCode = status; });Betiğin ele aldığı durumlar:
- Güncel fiyat
maxPricesınırını aşarsa numara alınmaz veWRONG_MAX_PRICE:<fiyat>döner; hata mesajından güncel fiyatı okuyabilirsiniz. NO_NUMBERSgeçici stok yokluğudur,NO_BALANCEbakiye yüklemeyi gerektirir. Ağ hataları, HTTP 429 ve 5xx iseTransientolarak ayrılır ve yoklama döngüsünde bekleme süresi katlanarak (en çok 60 sn) yeniden denenir.STATUS_WAIT_CODEveSTATUS_WAIT_RETRY:<eski kod>ikisi de "beklemeye devam" demektir; ikincisi önceki SMS'in kodunu taşır.- 20 dakika dolunca 8 ile iptal edilir; son saniyede kod gelmişse
EARLY_CANCEL_DENIEDdöner ve kod tekrar okunur. Kod alınınca 6 ile aktivasyon kapatılır.
JSON uçları: getNumberV2 ve getStatusV2
Nesne olarak çalışmak isterseniz getNumberV2 JSON döner ve JSON.parse ile okunur: activationId, phoneNumber, activationCost, currency, activationEndTime. getStatusV2 kod geldiğinde sms.code ve sms.text içeren JSON verir. Ancak hata kodları V2'de de düz metindir ve kod gelmeden önce getStatusV2 de düz metin durum döner; bu nedenle JSON.parse başarısız olursa metni durum olarak kullanan bir try/catch şarttır. activationEndTime sunucu saatindedir (Europe/Istanbul, UTC+3), Y-m-d H:i:s biçimindedir ve saat dilimi içermez.
async function getNumberV2(service, country, maxPrice = null) {
const text = await call("getNumberV2", {
service,
country,
maxPrice: maxPrice == null ? null : maxPrice.toFixed(2),
});
try {
return JSON.parse(text); // activationId, phoneNumber, activationCost, activationEndTime ...
} catch {
throw new ApiError(text); // errors stay plain text, even in V2
}
}
// Object with the SMS once it arrived, otherwise the plain-text status (a string).
async function getStatusV2(id) {
const text = await call("getStatusV2", { id });
let data;
try {
data = JSON.parse(text);
} catch {
return text; // STATUS_WAIT_CODE, STATUS_WAIT_RETRY:<old>, STATUS_CANCEL ...
}
return { code: data?.sms?.code ?? null, text: data?.sms?.text ?? null };
}
(async () => {
const order = await getNumberV2("wa", "62", 15.0);
console.log(order.activationId, order.phoneNumber, order.activationCost, "until", order.activationEndTime);
})();İkinci SMS isteme (setStatus 3)
Aynı numaraya yeni bir SMS gerektiğinde setStatus ile 3 gönderin. İşlem ücretsizdir; aktivasyon başına yalnızca bir kez ödersiniz. Yanıt ACCESS_RETRY_GET olur ve yeni SMS gelene kadar getStatus eski kodla STATUS_WAIT_RETRY döner; waitForCode() bunu bekleme sayar. Aktivasyonu 6 ile ancak son kodu aldıktan sonra kapatın. Aşağıdaki parça bir async fonksiyonun içinde çalışır.
// Inside an async function: you have a first code and need a second SMS on the same number (free).
const firstCode = await waitForCode(id);
const reply = await requestAnotherSms(id); // setStatus status=3 -> ACCESS_RETRY_GET
if (reply === "ACCESS_RETRY_GET") {
// While the new SMS is on its way getStatus answers STATUS_WAIT_RETRY:<first code>,
// which waitForCode() treats as "still waiting".
const secondCode = await waitForCode(id);
console.log("First:", firstCode, "Second:", secondCode);
}
await finish(id); // close only after the LAST codeWebhook alıcısı: Express ve imza doğrulama
Yoklama yerine bildirim isterseniz profilinizdeki API bölümü üzerinden HTTPS bir webhook adresi girin (alan adı, port 443 veya 8443). Kod geldiğinde (code_received) ya da aktivasyon iptal edildiğinde (cancelled) imzalı bir JSON POST alırsınız. Başlıklar X-Turonay-Event, X-Turonay-Timestamp, X-Turonay-Signature ve X-Turonay-Delivery olup imza, sha256= + HMAC-SHA256(gizli anahtar, zaman damgası + "." + ham gövde) biçimindedir.
// webhook.js npm install express | TURONAY_WEBHOOK_SECRET=whsec_... node webhook.js
const crypto = require("node:crypto");
const express = require("express");
const SECRET = process.env.TURONAY_WEBHOOK_SECRET; // whsec_... from your profile page
const seen = new Set(); // demo only: use Redis/DB with a TTL
// sha256=HMAC_SHA256(secret, timestamp + "." + raw body), 5 minute replay window.
function verifyWebhookSignature(secret, timestamp, rawBody, signature, toleranceSec = 300) {
if (!/^[0-9]{1,12}$/.test(timestamp ?? "")) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSec) return false;
const expected = "sha256=" + crypto.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from((signature ?? "").trim());
return a.length === b.length && crypto.timingSafeEqual(a, b); // constant time
}
const app = express();
// express.raw keeps the exact bytes; express.json() would re-serialise them.
app.post("/turonay/webhook", express.raw({ type: "*/*", limit: "64kb" }), (req, res) => {
if (!SECRET) return res.sendStatus(500);
const raw = req.body;
if (!Buffer.isBuffer(raw)) return res.sendStatus(400);
if (!verifyWebhookSignature(SECRET, req.get("X-Turonay-Timestamp"), raw, req.get("X-Turonay-Signature"))) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(raw.toString("utf8"));
} catch {
return res.sendStatus(400);
}
if (!seen.has(event.id)) { // retries repeat the same delivery id
seen.add(event.id);
if (event.event === "code_received") {
console.log("code", event.code, "for activation", event.activationId, event.phone);
} else if (event.event === "cancelled") {
console.log("activation", event.activationId, "cancelled, nothing charged");
}
}
res.sendStatus(200); // answer 2xx within 5 seconds
});
app.listen(8080, () => console.log("listening on :8080 (put HTTPS in front)"));Express'te en sık yapılan hata express.json() kullanmaktır: gövde ayrıştırılıp yeniden serileştirilince imza tutmaz. Bu yüzden express.raw() ile ham Buffer alın. crypto.timingSafeEqual iki tamponun uzunluğu eşit değilse hata verir; önce uzunlukları karşılaştırın. 5 dakikadan eski zaman damgalarını reddedin, 5 saniye içinde 2xx dönün ve aynı id yeniden gelirse atlayın (en çok 8 yeniden deneme yapılır). Ayrıntılar için Webhook sayfasına bakın.
Hata kodları
Uygulama hataları HTTP 200 ile düz metin olarak gelir; ağ hatası, 429 ve 5xx ise örnekteki Transient sınıfına dönüştürülür. Tabloda yanıt gövdesinde görebileceğiniz kodlar ve önerilen tepkiler var.
| Kod | Anlamı | Ne yapmalı |
|---|---|---|
BAD_KEY | API anahtarı boş, yanlış ya da engelli. | Anahtarı profilinizden yeniden kopyalayın; başında veya sonunda boşluk kalmadığından emin olun. |
NO_BALANCE | Kullanılabilir bakiye, numara fiyatından düşük. | Bakiye yükleyin. Rezerve edilmiş tutarlar kullanılabilir bakiyeden zaten düşülmüştür. |
NO_NUMBERS | O servis ve ülke için şu an numara yok (bakım sırasında da dönebilir). | Birkaç dakika sonra yeniden deneyin ya da başka bir ülke seçin. |
BAD_SERVICE | Servis kodu ya da ülke numarası geçersiz veya satışa kapalı. | Geçerli kodları getPrices ve getCountries uçlarından kontrol edin. |
WRONG_MAX_PRICE:<price> | Güncel fiyat maxPrice sınırınızdan yüksek (ya da sınır geçersiz). Numara alınmaz, para rezerve edilmez. | Koddaki iki noktadan sonra gelen güncel fiyatı okuyun; kabul ediyorsanız sınırı yükseltip yeniden deneyin. |
NO_ACTIVATION | Aktivasyon numarası yok ya da hesabınıza ait değil. | getNumber yanıtındaki id değerini aynen kullanın. |
BAD_STATUS | Geçersiz durum ya da kod gelmeden gönderilen status=6. | Önce kodu bekleyin; yalnızca 1, 3, 6 ve 8 gönderin. |
EARLY_CANCEL_DENIED | Kod zaten geldiği için iptal edilemez. | Kodu getStatus ile okuyun ve aktivasyonu 6 ile kapatın; ücret kesindir. |
NO_CONNECTION / HTTP 429 | Geçici sorun ya da hız sınırı. | Bekleme süresini katlayarak (en çok 60 saniye) yeniden deneyin ve istek sıklığını azaltın. |
STATUS_CANCEL | Aktivasyon iptal edildi (sizin tarafınızdan ya da tedarikçi tarafından). | Beklemeyi bırakın; ücret alınmaz. Gerekirse yeni numara alın. |
Mevcut handler_api istemcisini Turonay'a yönlendirme
handler_api düzenini kullanan bir Node.js botunuz ya da kütüphaneniz varsa yeniden yazmanız gerekmeyebilir: çoğu entegrasyonda temel adresi Turonay'ın /stubs/handler_api.php adresi, anahtarı da Turonay API anahtarınız yapmak yeterlidir. Adresi .env dosyasından ya da ortam değişkeninden okumak geçişi tek satıra indirir.
// Before: const BASE_URL = "https://provider.example.com/stubs/handler_api.php";
const BASE_URL = process.env.SMS_API_URL ?? "https://turonay.com/stubs/handler_api.php";
const API_KEY = process.env.SMS_API_KEY; // your Turonay API key
// Libraries and bots with a base-URL option need only these two values, e.g.:
// const client = new SomeHandlerApiClient({ apiKey: API_KEY, baseUrl: BASE_URL });Fiyatların Türk lirası olduğunu ve servis kodları ile ülke numaralarının getPrices ve getCountries çıktısına uyması gerektiğini unutmayın. Fiyatlar için API fiyatları sayfasına bakın.
Sık sorulan sorular
Node.js'te handler_api uyumlu API nasıl kullanılır?
Turonay handler_api protokolünü konuşur. Yerleşik fetch ile API adresine GET isteği atar, api_key ve action parametrelerini gönderir, düz metin yanıtı ayrıştırırsınız. Hazır bir kütüphaneniz varsa yalnızca temel adresi ve anahtarı değiştirmeniz yeterlidir.
axios ya da node-fetch kullanmam gerekir mi?
Hayır. Node.js 18 ve sonrasında fetch yerleşiktir. Axios kullanmayı tercih ederseniz mantık aynı kalır; yalnızca yanıtı JSON yerine düz metin olarak okumayı unutmayın.
SMS gelmezse ücret öder miyim?
Hayır. getNumber fiyatı yalnızca rezerve eder, ücret SMS kodu geldiğinde alınır. Kod gelmeden iptal ederseniz ya da 20 dakika dolarsa rezervasyon serbest kalır.
Webhook için neden express.raw gerekiyor?
İmza, istek gövdesinin birebir baytları üzerinden hesaplanır. express.json() gövdeyi ayrıştırıp yeniden serileştirdiğinde boşluk ve alan sırası değişebilir ve imza doğrulaması başarısız olur. Ham gövdeyi önce doğrulayın, sonra JSON'a çevirin.
Aynı numaradan ikinci SMS alabilir miyim?
Evet. setStatus çağrısında status=3 göndererek aynı numaraya yeni SMS isteyebilirsiniz. Bu ücretsizdir; aktivasyon başına yalnızca bir kez ödersiniz. Son kodu aldıktan sonra status=6 ile aktivasyonu kapatın.