PHP ile SMS Kodu Alma
Bu sayfada PHP ve cURL eklentisiyle Turonay SMS onay API'sini kullanmanın tam, kopyalanabilir bir örneğini bulacaksınız: sanal numara satın al, gelen doğrulama kodunu oku, aktivasyonu kapat veya iptal et. Turonay handler_api düzenini kullandığı için aynı PHP kodu bu protokolü bilen mevcut betikler ve paketlerle de uyumludur.
Örnek, Composer paketi gerektirmeden PHP 7.4 ve sonrasında çalışır; yalnızca cURL eklentisi yeterlidir. Önce bakiye sorgusuyla başlıyor, ardından tam akışı, JSON uçlarını, webhook alıcısını ve hata yönetimini ekliyoruz. Genel tanıtım için SMS API, tüm uç noktalar için API dokümanı sayfasına göz atabilirsiniz.
Gerekenler
Kodu denemeden önce şunları hazırlayın:
- Turonay API anahtarı: hesabınıza girip profilinizdeki API bölümü içinden kopyalayın. PHP kodunuzda anahtarı
getenv()ile ortam değişkeninden okuyun, depoya (repoya) yazmayın. - Bakiye: numara alındığında fiyat bakiyenizden rezerve edilir. Tüm tutarlar Türk lirası (TRY) cinsindendir.
- PHP 7.4 veya üstü ve etkin
curleklentisi (paylaşımlı hostinglerde genellikle hazır gelir).
getNumber fiyatı yalnızca rezerve eder; asıl tahsilat SMS kodu geldiği an yapılır. Kod gelmeden iptal ederseniz ya da 20 dakikalık süre dolarsa rezervasyon çözülür ve bedel ödemezsiniz. Kod geldikten sonra iptal EARLY_CANCEL_DENIED ile reddedilir.Kurulum: cURL eklentisini kontrol edin
Ek paket kurmanız gerekmez. Komut satırından PHP sürümünüzü ve curl eklentisinin yüklü olduğunu doğrulayın, ardından API anahtarını ortam değişkenine yazın. Kodu web sunucusunda çalıştıracaksanız aynı değişkeni PHP-FPM ya da sanal host ayarında tanımlayın.
php -v # PHP 7.4 or newer (8.x recommended)
php -m | grep -i curl # the curl extension must be listed
export TURONAY_API_KEY="paste-your-api-key-here"İlk çağrı: bakiyeyi okuyun
Bağlantıyı denemenin en kısa yolu getBalance isteğidir. Yanıt JSON değil düz metindir ve başarıda ACCESS_BALANCE:125.4000 biçimindedir; yanlış anahtar BAD_KEY döner. cURL ile gelen gövdede satır sonu olabileceğinden trim() kullanmayı unutmayın.
<?php
$url = 'https://turonay.com/stubs/handler_api.php?' . http_build_query([
'api_key' => getenv('TURONAY_API_KEY'),
'action' => 'getBalance',
]);
$ch = curl_init($url);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20]);
$text = trim((string) curl_exec($ch)); // plain text, not JSON: "ACCESS_BALANCE:125.4000"
if (strpos($text, 'ACCESS_BALANCE:') === 0) {
echo 'Balance (TRY): ', substr($text, strlen('ACCESS_BALANCE:')), PHP_EOL;
} else {
echo 'Error: ', $text, PHP_EOL; // e.g. BAD_KEY
}PHP ile tam akış: numara al, kodu yokla, tamamla
Aşağıdaki dosya tam yaşam döngüsünü içerir. getNumber() fiyat tavanıyla (maxPrice) alım yapar, waitForCode() her 5 saniyede getStatus sorgular ve en çok 20 dakika bekler; sonuca göre finish() ya da cancel() çalışır. Fonksiyon adları Turonay'ın PHP referans istemcisiyle aynıdır, dolayısıyla ileride bir sınıfa çevirmek kolaydır. Komut satırından php sms_flow.php ile çalıştırın.
<?php
declare(strict_types=1);
// sms_flow.php - buy a number, wait for the SMS code, finish or cancel. Needs ext-curl.
const API = 'https://turonay.com/stubs/handler_api.php';
/** The API answered with a plain-text error code (NO_BALANCE, NO_NUMBERS, ...). */
class ApiError extends RuntimeException {}
/** Network trouble, HTTP 429 or 5xx, NO_CONNECTION: safe to retry later. */
class Transient extends ApiError {}
class CodeTimeout extends RuntimeException {}
function apiCall(string $action, array $params = []): string
{
$key = (string) getenv('TURONAY_API_KEY');
$params = array_filter($params, static function ($v) { return $v !== null; });
$query = ['api_key' => $key, 'action' => $action] + $params;
$ch = curl_init(API . '?' . http_build_query($query));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $http !== 200) {
throw new Transient('HTTP ' . $http); // never print the URL: it holds your api_key
}
$text = trim((string) $body); // always trim: bodies are plain text
if ($text === 'NO_CONNECTION') {
throw new Transient($text);
}
return $text;
}
function getBalance(): string
{
$text = apiCall('getBalance');
if (strpos($text, 'ACCESS_BALANCE:') !== 0) {
throw new ApiError($text);
}
return substr($text, strlen('ACCESS_BALANCE:'));
}
/** @return array [activationId, phone]. The price is only RESERVED here, not charged. */
function getNumber(string $service, string $country, ?float $maxPrice = null): array
{
$text = apiCall('getNumber', [
'service' => $service,
'country' => $country,
'maxPrice' => $maxPrice === null ? null : number_format($maxPrice, 2, '.', ''),
]);
$parts = explode(':', $text);
if ($parts[0] === 'ACCESS_NUMBER' && count($parts) >= 3 && $parts[1] !== '' && $parts[2] !== '') {
return [$parts[1], $parts[2]];
}
throw new ApiError($text); // NO_BALANCE, NO_NUMBERS, BAD_SERVICE, WRONG_MAX_PRICE:<price> ...
}
/** @return array [state, code] where state is 'wait', 'ok' or 'cancel'. */
function getStatus(string $id): array
{
$text = apiCall('getStatus', ['id' => $id]);
if (strpos($text, 'STATUS_OK:') === 0) {
return ['ok', substr($text, strlen('STATUS_OK:'))];
}
if ($text === 'STATUS_CANCEL') {
return ['cancel', null];
}
if ($text === 'STATUS_WAIT_CODE' || $text === 'STATUS_WAIT_RESEND' || strpos($text, 'STATUS_WAIT_RETRY:') === 0) {
return ['wait', null]; // WAIT_RETRY carries the OLD code: still waiting for the new one
}
throw new ApiError($text); // NO_ACTIVATION, BAD_KEY ...
}
function setStatus(string $id, int $status): string
{
return apiCall('setStatus', ['id' => $id, 'status' => $status]);
}
function requestAnotherSms(string $id): string { return setStatus($id, 3); } // free, ACCESS_RETRY_GET
function finish(string $id): string { return setStatus($id, 6); } // ACCESS_ACTIVATION, only after a code
function cancel(string $id): string { return setStatus($id, 8); } // ACCESS_CANCEL / EARLY_CANCEL_DENIED
/** Poll every $interval seconds for at most $timeout seconds (20 minutes). */
function waitForCode(string $id, int $timeout = 1200, int $interval = 5): string
{
$deadline = microtime(true) + $timeout;
$delay = $interval;
while (microtime(true) < $deadline) {
try {
[$state, $code] = getStatus($id);
$delay = $interval;
if ($state === 'ok') {
return (string) $code;
}
if ($state === 'cancel') {
throw new ApiError('STATUS_CANCEL');
}
} catch (Transient $e) {
$delay = min($delay * 2, 60); // back off on HTTP 429 / network trouble
}
$left = $deadline - microtime(true);
if ($left <= 0) {
break;
}
sleep((int) max(1, min($delay, ceil($left))));
}
throw new CodeTimeout("no SMS within $timeout seconds");
}
function main(): int
{
[$service, $country] = ['wa', '62']; // WhatsApp, Turkey (see getPrices / getCountries)
try {
echo 'Balance (TRY): ', getBalance(), PHP_EOL;
[$id, $phone] = getNumber($service, $country, 15.00);
} catch (Transient $e) {
echo 'Temporary problem, try again later: ', $e->getMessage(), PHP_EOL;
return 1;
} catch (ApiError $e) {
$reason = $e->getMessage();
if (strpos($reason, 'WRONG_MAX_PRICE') === 0) {
$pos = strpos($reason, ':');
echo 'Current price is above your limit: ', $pos === false ? '?' : substr($reason, $pos + 1), PHP_EOL;
} elseif ($reason === 'NO_NUMBERS') {
echo 'No numbers in stock right now, try again in a minute', PHP_EOL;
} elseif ($reason === 'NO_BALANCE') {
echo 'Not enough balance, top up first', PHP_EOL;
} else {
echo 'API error: ', $reason, PHP_EOL;
}
return 1;
}
echo "Use this number: $phone (activation $id)", PHP_EOL;
try {
$code = waitForCode($id);
} catch (CodeTimeout $e) {
$reply = cancel($id); // nothing is charged when no code arrived
if ($reply !== 'EARLY_CANCEL_DENIED') {
echo "Timed out, activation cancelled: $reply", PHP_EOL;
return 1;
}
[, $code] = getStatus($id); // the code arrived in the last second
} catch (ApiError $e) { // STATUS_CANCEL: the supplier cancelled, nothing charged
echo 'Activation cancelled: ', $e->getMessage(), PHP_EOL;
return 1;
}
echo "SMS code: $code", PHP_EOL;
echo finish($id), PHP_EOL; // ACCESS_ACTIVATION
return 0;
}
exit(main());Kodun verdiği başlıca kararlar:
- Güncel fiyat
maxPricesınırınızı aşarsa numara alınmaz veWRONG_MAX_PRICE:<fiyat>döner; fiyatı okuyup sınırı bilerek yükseltebilirsiniz. NO_NUMBERSgeçici stok sorunudur;NO_BALANCEbakiye yüklemeyi gerektirir. İkisi deApiErrorolarak yakalanır, ağ ve hız sınırı sorunları iseTransientile ayrılır.STATUS_WAIT_CODEileSTATUS_WAIT_RETRY:<eski kod>birlikte "bekle" anlamına gelir; ikincisindeki kod önceki SMS'e aittir ve yeni kod diye kullanılmamalıdır.- 20 dakika dolarsa
setStatusile 8 gönderilerek iptal edilir; kod son anda gelmişseEARLY_CANCEL_DENIEDyanıtı üzerine kod yeniden okunur. Kod alındıysa 6 ile aktivasyon kapatılır.
JSON uçları: getNumberV2 ve getStatusV2
Alanlara adıyla erişmek isterseniz getNumberV2 yanıtı JSON verir ve json_decode ile diziye çevrilir: activationId, phoneNumber, activationCost, currency, activationEndTime. getStatusV2 kod geldiğinde sms.code ve sms.text içeren JSON döner. İki ayrıntıyı unutmayın: hata kodları V2'de de düz metindir ve kod gelene kadar getStatusV2 de düz metin durumları (STATUS_WAIT_CODE gibi) verir. Bu yüzden json_decode sonucu dizi değilse metni olduğu gibi işleyin. activationEndTime sunucu saatindedir (Europe/Istanbul, UTC+3), Y-m-d H:i:s biçimindedir ve saat dilimi içermez.
function getNumberV2(string $service, string $country, ?float $maxPrice = null): array
{
$text = apiCall('getNumberV2', [
'service' => $service,
'country' => $country,
'maxPrice' => $maxPrice === null ? null : number_format($maxPrice, 2, '.', ''),
]);
$data = json_decode($text, true);
if (!is_array($data)) {
throw new ApiError($text); // errors stay plain text, even in V2
}
return $data; // activationId, phoneNumber, activationCost, activationEndTime ...
}
/** @return array|string array with the SMS once it arrived, otherwise the plain-text status. */
function getStatusV2(string $id)
{
$text = apiCall('getStatusV2', ['id' => $id]);
$data = json_decode($text, true);
if (!is_array($data)) {
return $text; // STATUS_WAIT_CODE, STATUS_WAIT_RETRY:<old>, STATUS_CANCEL ...
}
return ['code' => $data['sms']['code'] ?? null, 'text' => $data['sms']['text'] ?? null];
}
$order = getNumberV2('wa', '62', 15.00);
echo $order['activationId'], ' ', $order['phoneNumber'], ' ', $order['activationCost'],
' until ', $order['activationEndTime'], PHP_EOL;İkinci SMS isteme (setStatus 3)
İlk SMS işinize yaramadıysa ya da servis ikinci bir doğrulama isterse aynı numaraya yeni SMS isteyebilirsiniz. setStatus çağrısına 3 verin: işlem ücretsizdir, aktivasyon başına yalnızca bir kez ödersiniz. Yanıt ACCESS_RETRY_GET olur; yeni SMS gelene kadar getStatus eski kodla birlikte STATUS_WAIT_RETRY döner ve waitForCode() bunu bekleme sayar. Aktivasyonu yalnızca son kodu aldıktan sonra 6 ile kapatın.
// The first code has arrived and you need a second SMS on the same number (free).
$firstCode = waitForCode($id);
$reply = 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".
$secondCode = waitForCode($id);
echo "First: $firstCode Second: $secondCode", PHP_EOL;
}
finish($id); // close only after the LAST codeWebhook alıcısı: düz PHP ve imza doğrulama
Yoklamak yerine Turonay'ın size haber vermesini isterseniz profilinizdeki API bölümü üzerinden HTTPS bir webhook adresi tanımlayın (alan adı, port 443 veya 8443). Kod geldiğinde (code_received) veya aktivasyon iptal edildiğinde (cancelled) adresinize imzalı bir JSON POST gelir. İmza X-Turonay-Signature başlığındadır ve sha256= + HMAC-SHA256(gizli anahtar, zaman damgası + "." + ham gövde) değerine eşittir; zaman damgası X-Turonay-Timestamp başlığında gelir.
<?php
declare(strict_types=1);
// turonay_webhook.php - public HTTPS endpoint on YOUR server (port 443 or 8443).
$secret = (string) getenv('TURONAY_WEBHOOK_SECRET'); // whsec_... from your profile page
/** sha256=HMAC_SHA256(secret, timestamp . "." . raw body), 5 minute replay window. */
function verifyWebhookSignature(string $secret, string $timestamp, string $rawBody, string $signature, int $tolerance = 300): bool
{
if (!preg_match('/^[0-9]{1,12}$/', $timestamp) || abs(time() - (int) $timestamp) > $tolerance) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, trim($signature)); // constant time
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
http_response_code(405);
exit;
}
if ($secret === '') {
http_response_code(500);
exit;
}
$raw = (string) file_get_contents('php://input'); // the exact bytes, before json_decode
if (strlen($raw) > 65536 || !verifyWebhookSignature(
$secret,
$_SERVER['HTTP_X_TURONAY_TIMESTAMP'] ?? '',
$raw,
$_SERVER['HTTP_X_TURONAY_SIGNATURE'] ?? ''
)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if (!is_array($event)) {
http_response_code(400);
exit;
}
// Retries repeat the same "id": store it (unique index) and skip events you already handled.
switch ($event['event'] ?? '') {
case 'code_received':
error_log("code {$event['code']} for activation {$event['activationId']} ({$event['phone']})");
break;
case 'cancelled':
error_log("activation {$event['activationId']} cancelled, nothing charged");
break;
}
http_response_code(200); // answer 2xx within 5 secondsPHP'de ham gövdeyi php://input ile okuyun, json_decode etmeden önce doğrulayın, karşılaştırmayı zamanlama saldırılarına karşı hash_equals ile yapın ve 5 dakikadan eski zaman damgalarını reddedin. 5 saniye içinde 2xx dönmezseniz teslimat yeniden denenir (en çok 8 kez), bu yüzden aynı id ikinci kez gelirse işleme almayın. Uzun işleri kuyruğa atıp hemen 200 dönün. Tüm olay alanları için Webhook sayfasına bakın.
Hata kodları
Uygulama hataları HTTP 200 ile düz metin gelir; ağ sorunu, 429 ve 5xx ise Transient istisnasına çevrilir ve güvenle yeniden denenebilir. Aşağıdaki tablo yanıt gövdesinde görebileceğiniz kodları ve önerilen tepkiyi listeler.
| 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. |
Var olan bir handler_api betiğini Turonay'a çevirme
handler_api düzenini kullanan bir PHP betiğiniz ya da paketiniz varsa çoğu entegrasyonda temel URL'yi Turonay'ın /stubs/handler_api.php adresine ve anahtarı Turonay anahtarınıza çevirmek yeterlidir; yine de canlıya almadan önce akışı test edin. Adres koda gömülüyse onu tek bir yapılandırma değerine taşımak ileride de işinizi kolaylaştırır.
<?php
// Before: define('SMS_API_URL', 'https://provider.example.com/stubs/handler_api.php');
define('SMS_API_URL', getenv('SMS_API_URL') ?: 'https://turonay.com/stubs/handler_api.php');
define('SMS_API_KEY', (string) getenv('SMS_API_KEY')); // your Turonay API key
// Scripts and libraries that build their own query string keep working unchanged:
// SMS_API_URL . '?api_key=' . SMS_API_KEY . '&action=getBalance'Geçiş sonrası fiyatların Türk lirası olduğunu, servis kodları ile ülke numaralarının getPrices ve getCountries çıktısına uyması gerektiğini unutmayın. Güncel fiyatlar için API fiyatları sayfasına bakın.
Sık sorulan sorular
PHP ile handler_api uyumlu API nasıl kullanılır?
Turonay handler_api protokolünü konuşur. cURL 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. Mevcut bir PHP betiğiniz varsa yalnızca temel adresi ve anahtarı değiştirmeniz yeterlidir.
PHP'de cURL yerine file_get_contents kullanabilir miyim?
Kullanabilirsiniz, ancak cURL zaman aşımını, bağlantı süresini ve HTTP durum kodunu daha net verir. Hız sınırı anlamına gelen HTTP 429 yanıtını ve ağ hatalarını ayırt etmek için cURL önerilir.
Kod gelmezse para kesilir mi?
Hayır. getNumber yalnızca rezervasyon yapar. Ücret SMS kodu geldiğinde alınır; kod gelmeden iptal ederseniz, tedarikçi iptal ederse ya da 20 dakika dolarsa tutar tekrar kullanılabilir bakiyenize döner.
Paylaşımlı hostingte yoklama döngüsü çalışır mı?
Komut satırı betiği olarak (cron ya da SSH) çalışır; web isteği içinde 20 dakika beklemek zaman aşımına takılır. Web uygulamalarında webhook kullanmak ya da yoklamayı arka plan işine taşımak daha doğrudur.
Aynı numaradan ikinci SMS nasıl istenir?
setStatus çağrısına status=3 verin. İşlem ücretsizdir ve aktivasyon başına yalnızca bir kez ödersiniz. Yanıt ACCESS_RETRY_GET olur; yeni kodu getStatus ile beklemeye devam edin ve aktivasyonu son koddan sonra status=6 ile kapatın.