Kısa özet: SMSCode API, katalogdaki gerçek bir
catalog_product_idile ücretli sipariş oluşturur. Her mantıksal sipariş için tek birIdempotency-Keyve tek bir istek gövdesi kullanın. Belirsiz bir ağ veya sunucu sonucunda aynı anahtar ile aynı gövdeyi yeniden gönderin; yeni bir sipariş oluşturmayın.
API erişimi
API anahtarınızı SMSCode hesabınızdan alın ve yalnızca sunucu tarafında saklayın. Anahtarı tarayıcı koduna, mobil uygulamaya, URL query parametresine veya loglara yazmayın.
https://api.smscode.gg/v1
Her istekte Bearer kimlik doğrulaması kullanılır:
Authorization: Bearer YOUR_API_TOKEN
Temel akış
/catalog/productsüzerinden sipariş verilebilir ürünü bulun.- Bir mantıksal istek için gövdeyi ve
Idempotency-Keydeğerini bir kez oluşturun. /orders/createile siparişi oluşturun./orders/{id}ile durumu sorgulayın veya imzalı webhook olaylarını işleyin.- Yalnızca
can_cancel: trueolduğunda/orders/cancelçağrısını yapın.
Katalogdan ürün seçme
Ürünler ülke ve platform kimlikleriyle filtrelenir. Yanıttaki catalog_product_id, yönlendirilmiş sipariş oluşturma için kullanılır.
GET /v1/catalog/products?country_id=7&platform_id=1&limit=100 HTTP/1.1
Host: api.smscode.gg
Authorization: Bearer YOUR_API_TOKEN
{
"success": true,
"data": [
{
"id": 1024,
"name": "WhatsApp - Indonesia",
"country_id": 7,
"platform_id": 1,
"available": 142,
"price": 750000,
"active": true,
"catalog_product_id": 88
}
],
"meta": {
"page": 1,
"limit": 100,
"count": 1
}
}
price IDR cinsinden tam sayıdır. Stok anlık değişebilir; katalogda görünen bir ürün sipariş anında yine de NO_OFFER_AVAILABLE döndürebilir.
Güvenli bir sipariş oluşturma
Aşağıdaki örnek tek seferlik bir oluşturmadır. Aynı mantıksal isteği ağ hatasından sonra yeniden denerseniz hem Idempotency-Key hem de JSON gövdesi byte düzeyinde aynı kalmalıdır.
POST /v1/orders/create HTTP/1.1
Host: api.smscode.gg
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
Idempotency-Key: logical-request-7f36f7cb
{"catalog_product_id":88,"quantity":1}
{
"success": true,
"data": {
"orders": [
{
"id": 90210,
"status": "ACTIVE",
"phone_number": "+6281234567890",
"otp_code": null,
"otp_received_at": null,
"expires_at": "2026-05-11T09:20:00+00:00",
"failed_reason": null,
"product_id": 1024,
"catalog_product_id": 88,
"operator_id": 42,
"operator_name": "Telkomsel",
"amount": 750000,
"can_finish": false,
"can_resend": false,
"can_cancel": false,
"can_replace": false,
"can_reactivate": false,
"resend_available_at": null,
"cancel_available_at": "2026-05-11T09:02:00+00:00",
"replace_available_at": "2026-05-11T09:02:00+00:00"
}
],
"failed_count": 0
}
}
Bu örnekte phone_number atanmıştır; alan atama tamamlanana kadar opsiyonel/nullable olabilir.
Çözümlenmiş siparişte değer eksik, null veya boşsa aynı tamsayı id için en fazla bir timeout’lu
GET /v1/orders/{id} yapın. Değer hâlâ boş olmayan bir string değilse ücretli create’i tekrarlamadan
yerel pending_assignment sonucunda durun ve numarayı hedef uygulamaya göndermeyin.
cURL örneğinde anahtar ve gövde ayrı değişkenlerde bir kez oluşturulur:
IDEMPOTENCY_KEY="$(uuidgen)"
REQUEST_BODY='{"catalog_product_id":88,"quantity":1}'
curl --fail-with-body --connect-timeout 5 --max-time 30 --request POST "https://api.smscode.gg/v1/orders/create" \
--header "Authorization: Bearer $SMSCODE_API_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data-binary "$REQUEST_BODY"
Sipariş durumunu ve SMS teslimini okuma
Geçerli durumlar yalnızca ACTIVE, OTP_RECEIVED, COMPLETED, CANCELED ve EXPIRED
değerleridir. Her sipariş görünümü için revision durumunu -1 ile başlatın ve render çağrıları arasında
sipariş kimliğine göre saklayın. Boolean olmayan açık bir tamsayı sms_revision yalnızca önceki
değerden kesin olarak büyük ve otp_message boş olmayan bir string ise mesajı tüketin ve durumu
ilerletin. Bu karar OTP_RECEIVED veya otp_code alanına bağlı değildir ve terminal durum kontrolünden
önce verilir; geçersiz, eski/eşit revision ya da boş mesaj revision durumunu ilerletmez.
GET /v1/orders/90210 HTTP/1.1
Host: api.smscode.gg
Authorization: Bearer YOUR_API_TOKEN
V1OrderSummary projeksiyonu (seçili alanlar; tam wire response değildir):
{
"success": true,
"data": {
"id": 90210,
"status": "OTP_RECEIVED",
"otp_code": null,
"otp_message": "Giriş bağlantınız: https://example.com/confirm",
"sms_revision": 1,
"otp_received_at": "2026-05-11T09:01:30+00:00",
"can_cancel": false
}
}
Aşağıdaki iki fonksiyon polling sonucunu ve iptal kararını API alanlarından üretir:
def classify_order_snapshot(order: dict) -> dict:
status = order["status"]
order_id = order["id"]
revision_by_order = getattr(
classify_order_snapshot,
"_revision_by_order",
{},
)
classify_order_snapshot._revision_by_order = revision_by_order
last_seen_revision = -1
if order_id in revision_by_order:
last_seen_revision = revision_by_order[order_id]
revision = order.get("sms_revision")
message = order.get("otp_message")
# OTP_RECEIVED ve otp_code mesaj tüketimi için kapı değildir.
if (
type(revision) is int
and revision > last_seen_revision
and isinstance(message, str)
and message.strip()
):
revision_by_order[order_id] = revision
return {
"outcome": "delivered",
"otp_code": order.get("otp_code"),
"otp_message": message,
"sms_revision": revision,
}
if status == "COMPLETED":
return {"outcome": "completed"}
if status == "CANCELED":
return {"outcome": "canceled"}
if status == "EXPIRED":
return {"outcome": "expired"}
return {"outcome": "active"}
def cancellation_decision(order: dict) -> str:
return "cancel" if order["can_cancel"] else "wait"
COMPLETED, teslim sinyali değildir. Bir terminal snapshot hâlâ tüketilmemiş daha yeni bir SMS
taşıyabilir; bu nedenle revision tüketimi her zaman COMPLETED, CANCELED ve EXPIRED dallarından
önce gelir.
Siparişi iptal etme
İptal için önce en güncel sipariş görünümündeki can_cancel değerini kontrol edin. SMS teslim edilmişse iptal ve iade uygunluğu kapanır.
POST /v1/orders/cancel HTTP/1.1
Host: api.smscode.gg
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json
{"id":90210}
CANCEL_TOO_EARLY minimum bekleme süresinin dolmadığını, CONFLICT siparişin iptal edilebilir durumda olmadığını belirtir. Bu sonuçlarda yeni bir sipariş oluşturarak hatayı gizlemeyin.
Hata sınıflandırması ve yeniden deneme
İyi biçimlenmiş hata gövdesindeki error.code alanına göre karar verin; serbest biçimli message metnine göre branch etmeyin.
| Kod | Oluşturma davranışı |
|---|---|
NO_OFFER_AVAILABLE |
Kesin iş reddi; farklı ürün/ülke için yeni gövde ve yeni anahtar kullanılabilir. |
VALIDATION_ERROR |
Kesin iş reddi; isteği düzeltmeden yeniden göndermeyin. |
PROVIDER_ERROR |
Kesin iş reddi; alternatif seçilecekse yeni mantıksal istek oluşturun. |
IDEMPOTENCY_KEY_REUSED |
Kesin iş reddi; aynı anahtar farklı gövdeyle kullanılmıştır. |
INSUFFICIENT_BALANCE |
İlk ücretli POST’tan sonra durun; failover yapmayın. |
REQUEST_IN_PROGRESS |
Kısa ve sınırlı beklemeden sonra aynı anahtar ve aynı gövdeyle deneyin. |
Diğer bilinen kodların tamamı ile gelecekte eklenecek bilinmeyen kodlar belirsiz sonuç olarak kapanır: yeni ülkeye geçmeyin, gövdeyi veya anahtarı değiştirmeyin. Bozuk JSON, kesilmiş yanıt ve geçersiz UTF-8 bir iş hatası değildir; mevcut anahtar, tam gövde, ülke ve tüm deneme geçmişiyle belirsiz sonucu kaydedin.
Retry-After yoksa veya pozitif bir tamsayı olarak ayrıştırılamıyorsa sınırlı varsayılan gecikme
kullanın. Buradaki 60 saniyelik üst sınır istemciye ait yerel bir politikadır; API kotası veya SLA
değildir:
def retry_after_seconds(headers: dict, fallback: int = 2) -> int:
try:
fallback_seconds = int(fallback)
except (TypeError, ValueError, OverflowError):
fallback_seconds = 2
bounded_fallback = max(1, min(fallback_seconds, 60))
raw_value = headers.get("Retry-After")
try:
seconds = int(raw_value)
except (TypeError, ValueError, OverflowError):
return bounded_fallback
if seconds <= 0:
return bounded_fallback
return max(1, min(seconds, 60))
Webhook imzası ve işleme sırası
Webhook secret ayarlıysa X-Webhook-Signature değeri sha256={hex} biçimindedir. İmzayı parse edilmiş JSON üzerinden değil, gelen ham request byte’ları üzerinden doğrulayın. Sıra şu olmalıdır: doğrula, parse et, normalize et, atomik olarak kalıcı inbox’a yaz, sonra 2xx döndür; iş etkilerini ayrı bir worker çalıştırır.
import hashlib
import hmac
import json
import os
from flask import Flask, jsonify, request
from myapp.inbox import durable_inbox
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["SMSCODE_WEBHOOK_SECRET"].encode("utf-8")
def verify_signature(raw_body: bytes, signature: str) -> bool:
if not signature.startswith("sha256="):
return False
try:
provided = bytes.fromhex(signature.removeprefix("sha256="))
except ValueError:
return False
expected = hmac.new(WEBHOOK_SECRET, raw_body, hashlib.sha256).digest()
return hmac.compare_digest(expected, provided)
def normalize_event(event: dict) -> dict:
data = event["data"]
if event["event"] == "webhook.test":
return {"event": event["event"], "message": data["message"]}
return {
"event": event["event"],
"order_id": data["order_id"],
"otp_message": data["otp_message"],
"sms_revision": data["sms_revision"],
}
def webhook_dedupe_key(event: dict, raw_body: bytes) -> str:
data = event["data"]
if event["event"] == "webhook.test":
return f'{event["event"]}:{hashlib.sha256(raw_body).hexdigest()}'
if event["event"] == "order.otp_received":
return f'{event["event"]}:{data["order_id"]}:{data["sms_revision"]}'
return f'{event["event"]}:{data["order_id"]}'
def persist_or_enqueue_once(dedupe_key: str, event_record: dict) -> None:
durable_inbox.insert_if_absent(dedupe_key, event_record)
@app.post("/webhooks/smscode")
def smscode_webhook():
raw_body = request.get_data(cache=True, as_text=False)
signature = request.headers.get("X-Webhook-Signature", "")
if not verify_signature(raw_body, signature):
return jsonify({"error": "invalid signature"}), 401
event = json.loads(raw_body)
dedupe_key = webhook_dedupe_key(event, raw_body)
event_record = normalize_event(event)
persist_or_enqueue_once(dedupe_key, event_record)
return jsonify({"status": "accepted"}), 202
Olası sipariş olayları order.otp_received, order.completed, order.expired ve order.canceled değerleridir. insert_if_absent çağrısı unique anahtar üzerinde atomik olmalıdır: OTP olayı için event + order_id + sms_revision, terminal olay için event + order_id, webhook.test için ham gövdenin SHA-256 değeri kullanılır. Request handler yalnızca olayı normalize edip kalıcı inbox’a yazar; iş etkilerini inbox’ı tüketen ayrı bir worker’da çalıştırın. Kalıcı kayıt veya kuyruklama başarısızsa 2xx döndürmeyin.
Güvenlik kontrol listesi
- API ve webhook secret değerlerini environment secret store içinde tutun.
- API anahtarını URL’ye, istemci tarafı koda veya loga koymayın.
- Her HTTP çağrısına bağlantı ve toplam timeout uygulayın.
- Ücretli create sonucunda belirsizlik varsa yeni sipariş oluşturmayın.
Idempotency-Key, tam request gövdesi ve deneme geçmişini birlikte saklayın.- Sipariş kimliğini ve
sms_revisiondeğerini loglayın; token, telefon ve mesaj metnini loglamayın.
FAQ
Resmî SDK gerekli mi?
Hayır. API standart HTTPS ve JSON kullanır. HTTP istemcisi olan her dil kullanılabilir; bu rehberdeki sözleşmeleri kendi istemcinizde korumanız gerekir.
otp_code null ise SMS gelmedi mi?
Hayır. Teslimatı status veya otp_code alanına bağlamayın. Sipariş için tutulan revision değerinden
kesin olarak büyük, boolean olmayan bir tamsayı sms_revision ile boş olmayan string otp_message
birlikteyse mesajı terminal durum kontrolünden önce tüketip revision durumunu ilerletin. otp_code bu
sırada null olabilir.
Timeout sonrası hemen iptal etmeli miyim?
Hayır. Önce siparişi tekrar okuyun ve yalnızca can_cancel: true olduğunda iptal çağrısı yapın. Belirsiz create yanıtını iptal edilecek bir sipariş yokmuş gibi yorumlamayın.
Tam endpoint ve schema sözleşmesi için API dokümantasyonunu kullanın.
İlgili rehberler: