Ana sayfa › SMS API › Python ile SMS API
Python · requests · SMS Onay API

Python ile SMS Kodu Alma

Bu rehber, Python ile Turonay SMS onay API'sini baştan sona çalıştıran kopyala-çalıştır bir örnek sunar: numara al, SMS kodunu bekle, kodu yazdır, aktivasyonu kapat ya da süre dolarsa iptal et. Turonay handler_api protokolünü konuştuğu için aynı kod, bu protokolü kullanan botlar ve kütüphanelerle de uyumludur.

Sayfa sırayla ilerler: hazırlık, bakiye sorgusu, tam akış, JSON (V2) uçları, ikinci SMS, webhook ile imza doğrulama ve hata kodları. Genel API tanıtımı için SMS API sayfasına, tüm uç noktaların ayrıntısı için API dokümanı sayfasına bakabilirsiniz.

Başlamadan önce

Örneği çalıştırmak için üç şeye ihtiyacınız var:

  • Bir Turonay hesabı ve API anahtarı: kayıt olduktan sonra anahtarınızı profilinizdeki API bölümü içinden kopyalayın. Anahtarı yalnızca sunucunuzda, ortam değişkeninde tutun; tarayıcıya ya da mobil uygulamaya gömmeyin.
  • Bakiye: getNumber çağrısı numaranın fiyatını bakiyenizden rezerve eder. Tutarlar Türk lirası (TRY) üzerindendir.
  • Python 3.8 veya üstü ve requests kütüphanesi.
Ücretlendirme nasıl çalışır: Numara alındığında fiyat yalnızca rezerve edilir. Ücret, SMS kodu gerçekten geldiği anda tahsil edilir. Kod gelmeden iptal ederseniz ya da aktivasyon 20 dakikada dolarsa rezervasyon serbest bırakılır ve hiçbir şey ödemezsiniz. Kod geldikten sonra iptal reddedilir (EARLY_CANCEL_DENIED).

Kurulum: virtualenv ve requests

Sanal ortam oluşturup tek bağımlılığı kurmanız yeterli. Flask yalnızca aşağıdaki webhook alıcısı için gerekir; sadece API çağrıları yapacaksanız kurmayabilirsiniz. API anahtarını TURONAY_API_KEY ortam değişkeninden okuyacağız.

bash
python3 -m venv .venv && source .venv/bin/activate
pip install requests          # HTTP client used for every API call
pip install flask             # only needed for the webhook receiver below
export TURONAY_API_KEY="paste-your-api-key-here"

İlk istek: bakiyeyi sorgulayın

En küçük çalışan örnek getBalance çağrısıdır. API her zaman GET ile çalışır ve yanıt JSON değil düz metindir: başarılı yanıt ACCESS_BALANCE:125.4000 biçimindedir, hatalı anahtar BAD_KEY döner. Gövdeyi her zaman strip() ile temizleyin, çünkü sonunda satır sonu olabilir.

python
import os
import requests

API = "https://turonay.com/stubs/handler_api.php"

resp = requests.get(
    API,
    params={"api_key": os.environ["TURONAY_API_KEY"], "action": "getBalance"},
    timeout=20,
)
text = resp.text.strip()            # plain text, not JSON: "ACCESS_BALANCE:125.4000"
if text.startswith("ACCESS_BALANCE:"):
    print("Balance (TRY):", text.split(":", 1)[1])
else:
    print("Error:", text)            # e.g. BAD_KEY

Python ile tam akış: numara al, SMS kodunu bekle

Aşağıdaki betik tüm yaşam döngüsünü yürütür. get_number fiyat tavanıyla (maxPrice) numara alır, wait_for_code her 5 saniyede bir getStatus sorar ve en fazla 20 dakika bekler; sonuca göre finish ya da cancel çağrılır. Fonksiyon adları Turonay'ın Python referans istemcisiyle aynıdır, böylece örneği sonradan kendi projenizdeki bir sınıfa taşımak kolaydır.

python
#!/usr/bin/env python3
"""sms_flow.py - buy a number, wait for the SMS code, finish or cancel.  pip install requests"""
import os
import sys
import time
from decimal import Decimal

import requests

API = "https://turonay.com/stubs/handler_api.php"
API_KEY = os.environ["TURONAY_API_KEY"]


class ApiError(Exception):
    """The API answered with a plain-text error code (NO_BALANCE, NO_NUMBERS, ...)."""


class Transient(ApiError):
    """Network trouble, HTTP 429 or 5xx, NO_CONNECTION: safe to retry later."""


def call(action, **params):
    query = {k: v for k, v in params.items() if v is not None}
    query.update(api_key=API_KEY, action=action)
    try:
        resp = requests.get(API, params=query, timeout=20)
    except requests.RequestException as exc:
        # Do not chain the original error: its message contains your api_key.
        raise Transient("network error: " + type(exc).__name__) from None
    if resp.status_code != 200:
        raise Transient("HTTP %d" % resp.status_code)
    text = resp.text.strip()            # always strip: bodies are plain text
    if text == "NO_CONNECTION":
        raise Transient(text)
    return text


def get_balance():
    text = call("getBalance")
    if not text.startswith("ACCESS_BALANCE:"):
        raise ApiError(text)
    return Decimal(text.split(":", 1)[1])


def get_number(service, country, max_price=None):
    """Returns (activation_id, phone). The price is only RESERVED here, not charged."""
    text = call("getNumber", service=service, country=country,
                maxPrice=None if max_price is None else "%.2f" % max_price)
    parts = text.split(":")
    if parts[0] == "ACCESS_NUMBER" and len(parts) >= 3 and parts[1] and parts[2]:
        return parts[1], parts[2]
    raise ApiError(text)    # NO_BALANCE, NO_NUMBERS, BAD_SERVICE, WRONG_MAX_PRICE:<price> ...


def get_status(activation_id):
    """Returns ("wait" | "ok" | "cancel", code_or_None)."""
    text = call("getStatus", id=activation_id)
    if text.startswith("STATUS_OK:"):
        return "ok", text.split(":", 1)[1]
    if text == "STATUS_CANCEL":
        return "cancel", None
    if text in ("STATUS_WAIT_CODE", "STATUS_WAIT_RESEND") or text.startswith("STATUS_WAIT_RETRY:"):
        return "wait", None    # WAIT_RETRY carries the OLD code: still waiting for the new one
    raise ApiError(text)       # NO_ACTIVATION, BAD_KEY ...


def set_status(activation_id, status):
    return call("setStatus", id=activation_id, status=status)


def request_another_sms(activation_id):
    return set_status(activation_id, 3)    # free, ACCESS_RETRY_GET


def finish(activation_id):
    return set_status(activation_id, 6)    # ACCESS_ACTIVATION, only valid after a code


def cancel(activation_id):
    return set_status(activation_id, 8)    # ACCESS_CANCEL, or EARLY_CANCEL_DENIED after a code


def wait_for_code(activation_id, timeout=1200, interval=5):
    """Poll every `interval` seconds for at most `timeout` seconds (20 minutes)."""
    deadline = time.monotonic() + timeout
    delay = interval
    while time.monotonic() < deadline:
        try:
            state, code = get_status(activation_id)
            delay = interval
            if state == "ok":
                return code
            if state == "cancel":
                raise ApiError("STATUS_CANCEL")
        except Transient:
            delay = min(delay * 2, 60)       # back off on HTTP 429 / network trouble
        time.sleep(max(0, min(delay, deadline - time.monotonic())))
    raise TimeoutError("no SMS within %d seconds" % timeout)


def main():
    service, country = "wa", "62"            # WhatsApp, Turkey (see getPrices / getCountries)
    try:
        print("Balance (TRY):", get_balance())
        activation_id, phone = get_number(service, country, max_price=15.00)
    except Transient as exc:
        sys.exit("Temporary problem, try again later: %s" % exc)
    except ApiError as exc:
        reason = str(exc)
        if reason.startswith("WRONG_MAX_PRICE"):
            print("Current price is above your limit:", reason.partition(":")[2] or "?")
        elif reason == "NO_NUMBERS":
            print("No numbers in stock right now, try again in a minute")
        elif reason == "NO_BALANCE":
            print("Not enough balance, top up first")
        else:
            print("API error:", reason)
        sys.exit(1)

    print("Use this number: %s (activation %s)" % (phone, activation_id))
    try:
        code = wait_for_code(activation_id)
    except TimeoutError:
        reply = cancel(activation_id)        # nothing is charged when no code arrived
        if reply != "EARLY_CANCEL_DENIED":
            sys.exit("Timed out, activation cancelled: %s" % reply)
        code = get_status(activation_id)[1]  # the code arrived in the last second
    except ApiError as exc:                  # STATUS_CANCEL: the supplier cancelled, nothing charged
        sys.exit("Activation cancelled: %s" % exc)

    print("SMS code:", code)
    print(finish(activation_id))             # ACCESS_ACTIVATION


if __name__ == "__main__":
    main()

Betik şu kararları verir:

  • maxPrice ile fiyat tavanı koyarsınız. Güncel fiyat tavanınızdan yüksekse numara hiç alınmaz ve WRONG_MAX_PRICE:<güncel fiyat> döner; bu durumda yeni bir tavanla yeniden denersiniz.
  • NO_NUMBERS (stok yok) geçicidir, birkaç dakika sonra tekrar deneyin; NO_BALANCE ise bakiye yüklemeniz gerektiği anlamına gelir.
  • STATUS_WAIT_CODE ve STATUS_WAIT_RETRY:<eski kod> yanıtlarının ikisi de "hâlâ bekliyorum" demektir. İkincisi bir önceki SMS'in kodunu taşır; onu yeni kod sanmamak gerekir.
  • Süre dolarsa setStatus ile 8 gönderilir. Kod son saniyede geldiyse API EARLY_CANCEL_DENIED döner; betik bu durumda kodu bir kez daha okur ve işlemi tamamlar. Kod alındıysa 6 ile aktivasyon kapatılır.

JSON yanıtlar: getNumberV2 ve getStatusV2

Düz metin ayrıştırmak istemezseniz getNumberV2 numara bilgisini JSON olarak verir: activationId, phoneNumber, activationCost, currency ve activationEndTime alanları gelir. getStatusV2 ise kod geldikten sonra sms.code ve sms.text içeren JSON döner. İki noktaya dikkat edin: hatalar V2 uçlarında da düz metindir ve kod gelmeden önce getStatusV2 de getStatus ile aynı düz metni verir. Bu yüzden önce json.loads deneyip olmazsa metne düşmek en güvenli yoldur. activationEndTime sunucu saatindedir (Europe/Istanbul, UTC+3), Y-m-d H:i:s biçimindedir ve saat dilimi içermez.

python
import json


def get_number_v2(service, country, max_price=None):
    text = call("getNumberV2", service=service, country=country,
                maxPrice=None if max_price is None else "%.2f" % max_price)
    try:
        data = json.loads(text)
    except ValueError:
        raise ApiError(text)    # errors stay plain text, even in V2
    return data                 # activationId, phoneNumber, activationCost, activationEndTime ...


def get_status_v2(activation_id):
    """dict with the SMS once it arrived, otherwise the plain-text status (a str)."""
    text = call("getStatusV2", id=activation_id)
    try:
        data = json.loads(text)
    except ValueError:
        return text             # STATUS_WAIT_CODE, STATUS_WAIT_RETRY:<old>, STATUS_CANCEL ...
    sms = (data or {}).get("sms") or {}
    return {"code": sms.get("code"), "text": sms.get("text")}


order = get_number_v2("wa", "62", max_price=15.00)
print(order["activationId"], order["phoneNumber"], order["activationCost"], "until", order["activationEndTime"])

Aynı numaraya ikinci SMS isteme

İlk SMS işinize yaramadıysa ya da servis ikinci bir doğrulama isterse aynı numaraya yeni bir 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 ve yeni SMS gelene kadar getStatus eski kodla birlikte STATUS_WAIT_RETRY döner. Aktivasyonu 6 ile yalnızca son kodu aldıktan sonra kapatın.

python
# The first code has arrived and you need a second SMS on the same number (free).
first_code = wait_for_code(activation_id)
reply = request_another_sms(activation_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 wait_for_code() treats as "still waiting".
    second_code = wait_for_code(activation_id)
    print("First:", first_code, "Second:", second_code)
finish(activation_id)                             # close only after the LAST code

Webhook alıcısı: Flask ve imza doğrulama

Sürekli yoklama yapmak yerine Turonay'ın kodu size bildirmesini sağlayabilirsiniz. Bunun için profilinizdeki API bölümü üzerinden HTTPS bir adres girin (alan adı olmalı, port 443 ya da 8443); Turonay kod geldiğinde (code_received) ya da aktivasyon iptal edildiğinde (cancelled) imzalı bir JSON POST gönderir. Başlıklar: X-Turonay-Event, X-Turonay-Timestamp, X-Turonay-Signature ve X-Turonay-Delivery. İmza, zaman damgası + "." + ham gövde üzerinden HMAC-SHA256 ile hesaplanır ve sha256= önekiyle gelir.

python
# webhook_app.py   pip install flask   |   flask --app webhook_app run --port 8080
import hashlib
import hmac
import os
import time

from flask import Flask, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024            # events are tiny
SECRET = os.environ["TURONAY_WEBHOOK_SECRET"]           # whsec_... from your profile page
SEEN = set()                                            # demo only: use Redis/DB with a TTL


def verify_webhook_signature(secret, timestamp, raw_body, signature, tolerance=300):
    """sha256=HMAC_SHA256(secret, timestamp + "." + raw body), 5 minute replay window."""
    try:
        age = abs(time.time() - int(timestamp))
    except (TypeError, ValueError):
        return False
    if age > tolerance:
        return False
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256)
    expected = ("sha256=" + mac.hexdigest()).encode()
    return hmac.compare_digest(expected, (signature or "").strip().encode())   # constant time


@app.post("/turonay/webhook")
def turonay_webhook():
    raw = request.get_data()            # the exact bytes, before any JSON parsing
    if not verify_webhook_signature(
        SECRET,
        request.headers.get("X-Turonay-Timestamp", ""),
        raw,
        request.headers.get("X-Turonay-Signature", ""),
    ):
        return "", 401
    event = request.get_json(force=True, silent=True)
    if not isinstance(event, dict):
        return "", 400
    if event.get("id") in SEEN:         # retries repeat the same delivery id
        return "", 200
    SEEN.add(event.get("id"))
    if event.get("event") == "code_received":
        print("code", event["code"], "for activation", event["activationId"], event["phone"])
    elif event.get("event") == "cancelled":
        print("activation", event["activationId"], "cancelled, nothing charged")
    return "", 200                      # answer 2xx within 5 seconds; do slow work elsewhere

Güvenlik için üç kural: imzayı JSON'a çevirmeden önce ham baytlar üzerinden doğrulayın, karşılaştırmayı zamanlama saldırısına dayanıklı hmac.compare_digest ile yapın ve zaman damgası 5 dakikadan eski istekleri reddedin. 5 saniye içinde 2xx dönmezseniz Turonay teslimatı yeniden dener (en fazla 8 deneme), bu yüzden aynı olay iki kez gelebilir; id alanıyla tekrarı yok sayın. Tüm olay alanları için Webhook sayfasına bakın. Bir webhook kaçarsa wait_for_code döngüsü yedek olarak çalışmaya devam eder.

Hata kodları ve ne yapmalı

API, uygulama hatalarını HTTP 200 ile düz metin olarak döner; ağ ve hız sınırı sorunları ise HTTP durum koduyla gelir. Örnekteki Transient sınıfı yeniden denenebilecek durumları (ağ hatası, 429, 5xx, NO_CONNECTION) ayırır, ApiError ise kalıcı hataları taşır. Tablo en sık gördüğünüz kodları özetler.

KodAnlamıNe yapmalı
BAD_KEYAPI anahtarı boş, yanlış ya da engelli.Anahtarı profilinizden yeniden kopyalayın; başında veya sonunda boşluk kalmadığından emin olun.
NO_BALANCEKullanılabilir bakiye, numara fiyatından düşük.Bakiye yükleyin. Rezerve edilmiş tutarlar kullanılabilir bakiyeden zaten düşülmüştür.
NO_NUMBERSO 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_SERVICEServis 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_ACTIVATIONAktivasyon numarası yok ya da hesabınıza ait değil.getNumber yanıtındaki id değerini aynen kullanın.
BAD_STATUSGeçersiz durum ya da kod gelmeden gönderilen status=6.Önce kodu bekleyin; yalnızca 1, 3, 6 ve 8 gönderin.
EARLY_CANCEL_DENIEDKod zaten geldiği için iptal edilemez.Kodu getStatus ile okuyun ve aktivasyonu 6 ile kapatın; ücret kesindir.
NO_CONNECTION / HTTP 429Geç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_CANCELAktivasyon 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 istemcisini Turonay'a yönlendirme

handler_api protokolüyle yazılmış bir botunuz ya da kütüphaneniz varsa kodu yeniden yazmanız gerekmeyebilir: çoğu entegrasyonda temel (API) adresini Turonay'ın /stubs/handler_api.php ile biten adresine, anahtarı da Turonay API anahtarınıza çevirmek yeterlidir. Python'da bunu ortam değişkeniyle yapmak en temiz yoldur.

python
import os

# Before: BASE_URL = "https://provider.example.com/stubs/handler_api.php"
BASE_URL = os.environ.get("SMS_API_URL", "https://turonay.com/stubs/handler_api.php")
API_KEY = os.environ["SMS_API_KEY"]        # your Turonay API key

# Libraries and bots with a base-URL option need only these two values, e.g.:
# client = SomeHandlerApiClient(api_key=API_KEY, base_url=BASE_URL)

Geçişten önce iki şeyi kontrol edin: tutarlar Türk lirasıdır ve servis kodları ile ülke numaraları getPrices ve getCountries çıktısıyla uyuşmalıdır. Fiyatlar için API fiyatları sayfasına bakın.

Sık sorulan sorular

Python'da handler_api uyumlu API için hangi adresi kullanmalıyım?

Turonay handler_api protokolünü konuştuğu için istekleri Turonay'ın handler_api adresine, api_key ve action parametreleriyle GET olarak gönderirsiniz. Mevcut Python kodunuzda yalnızca temel adresi ve anahtarı değiştirmeniz yeterlidir; adresi yukarıdaki örneklerde görebilirsiniz.

SMS gelmezse ücret öder miyim?

Hayır. getNumber çağrısı fiyatı yalnızca rezerve eder. Ücret SMS kodu geldiği anda alınır; kod gelmeden iptal ederseniz, tedarikçi iptal ederse ya da 20 dakika dolarsa rezervasyon serbest bırakılır.

Python'da ne sıklıkla yoklama yapmalıyım?

Her 5 saniyede bir getStatus çağırmak yeterlidir; 3 saniyenin altına inmeyin. HTTP 429 alırsanız bekleme süresini iki katına çıkarın. Daha verimli çözüm webhook kullanmaktır; bu durumda döngü yalnızca yedek olarak kalır.

asyncio ya da aiohttp ile kullanabilir miyim?

Evet. Protokol sade GET isteklerinden oluşur, bu yüzden requests yerine aiohttp ya da httpx kullanabilirsiniz. Mantık aynı kalır: düz metni ayrıştırın, STATUS_WAIT_RETRY yanıtını bekleme sayın ve süreyi 20 dakikayla sınırlayın.

Aynı numaradan ikinci bir SMS alabilir miyim?

Evet. setStatus çağrısında status=3 göndererek aynı numaraya yeni SMS isteyebilirsiniz. Bu ücretsizdir ve aktivasyon başına yalnızca bir kez ödeme yaparsınız. Aktivasyonu son kodu aldıktan sonra status=6 ile kapatın.