DEVELOPER API

SMSGonder entegrasyon rehberi

TEST Sandbox’tan LIVE’a geçiş, OTP Messaging, OTP Verification, Hosted OTP, Delivery Report, Webhook, imza doğrulama, idempotency ve limitler tek sayfada.

QUICK START

Önce TEST, sonra yalnız URL’yi LIVE’a çevirin

Base URL: https://www.smsgonder.com.tr
Aynı API Key kullanılır. /test/* hiçbir zaman gerçek SMS provider’a çıkmaz; /api/* LIVE ortamıdır. TEST ve LIVE mesaj/OTP kimlikleri, SMS Credit ve kullanım sayaçları birbirinden izoledir.

1 · API Key oluşturun

Dashboard → API Keys. API Key için bir veya daha fazla Service Mode ve bir veya daha fazla APPROVED Template izni seçin. API Key yalnız ilk oluşturulduğunda tam olarak gösterilir.

Authorization: Authorization: Bearer sk_...

2 · Template key’i doğru kullanın

İstek içindeki template_key, API Key için izin verilmiş TemplateKey değerlerinden biriyle exact aynı olmalıdır.

Hazır örnekler login ve activation; bu dokümandaki örnekler activation kullanır.

3 · Sandbox senaryolarını çalıştırın

900000000001…005 numaraları ile delivered, failed, provider reject, timeout ve pending akışlarını deterministik test edin.

4 · LIVE’a geçin

Kodunuzdaki /test yolunu /api yapın. API Key aynı kalabilir; LIVE paket, SMS Credit ve gateway hazır olmalıdır.

İlk TEST çağrısı

curl --fail-with-body -X POST "https://www.smsgonder.com.tr/test/send" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-2026-000001" \
  --data '{"phone":"900000000001","template_key":"activation","verification_code":"123456"}'
CONTRACT

Entegrasyon kuralları

KonuKural
AuthenticationAuthorization: Bearer sk_.... API secret’ı browser/mobile client içine gömmeyin; yalnız server-side saklayın.
POST bodyContent-Type: application/json. UTF-8 kullanın. Bilinmeyen JSON alanları reddedilir.
TelefonLIVE ortamında şu anda yalnız Türkiye mobil numaraları desteklenir. Ülke kodu 90 zorunludur ve format 905XXXXXXXXX olmalıdır. +, baştaki ulusal 0, boşluk, tire ve parantez kullanmayın. Türkiye dışındaki numaralar şu anda desteklenmez.
TemplateHer API Key bir veya daha fazla APPROVED template için yetkilendirilebilir. İzinli olmayan key → TEMPLATE_NOT_ALLOWED.
Tek SMS segmentiRendered mesaj sınırı: DEFAULT 155, TURKISH 150, UNICODE 65 karakter. Aşılırsa MESSAGE_TOO_LONG.
Code policy4–8 karakter; NUMERIC veya ALPHANUMERIC. OTP Messaging’de kodu siz verirsiniz; Verification/Hosted’da SMSGonder üretir.
IdempotencyDış işlem yaratan çağrılarda aynı mantıksal işlem için aynı Idempotency-Key ile retry yapın. 8–128 printable ASCII.
Request IDHer API cevabındaki request_id değerini loglayın; destek/izleme için temel korelasyon kimliğidir.
LIVE telefon kapsamı: SMSGonder şu anda yalnız Türkiye mobil numaralarına gönderim yapar. LIVE formatı 905XXXXXXXXX şeklindedir. TEST Sandbox’taki 900000000001…005 numaraları yalnız deterministik test senaryolarıdır ve LIVE numara formatı değildir.
En kritik retry kuralı: Bir send/create çağrısı 500 INTERNAL_ERROR ve "status":"unknown" dönerse yeni bir mantıksal gönderim oluşturmayın. Aynı Idempotency-Key ve aynı body ile tekrar deneyin.
SERVICE MODE

API Key hangi endpoint’leri kullanabilir?

Service ModeAna endpointDavranış
OTP_MESSAGINGPOST /sendDoğrulama kodunu müşteri sistemi üretir ve request’te gönderir.
OTP_VERIFICATIONPOST /otp/create
POST /otp/check
Kodu SMSGonder üretir; müşteri otp_id ile doğrular.
HOSTED_OTPPOST /hosted/createKullanıcı SMSGonder hosted sayfasında kodu girer; sonuç imzalı form POST ile return_url adresine döner.

Endpoint kendi Service Mode’unu bilir; request içine service_mode gönderilmez. Aynı API Key birden fazla Service Mode iznine sahip olabilir. /report, /balance ve /usage Service Mode’dan bağımsızdır; ilgili API Key ayarları ve izinleri geçerlidir. Daha önce oluşturulmuş bir OTP, ilgili izin sonradan kaldırılsa bile aynı key ile check edilebilir.

IDEMPOTENCY

Retry yaptığınızda ikinci SMS gitmesin

DurumSonuçMüşteri davranışı
Aynı key + aynı bodyStored terminal response replay edilirAynı mantıksal retry için doğru kullanım.
Aynı key + farklı body409 IDEMPOTENCY_CONFLICTYeni mantıksal işlem için yeni key üretin.
İlk işlem hâlâ sürüyor409 IDEMPOTENCY_IN_PROGRESSKısa bekleyin ve aynı key/body ile yeniden deneyin.
Provider kabul etti fakat local finalize belirsiz500 INTERNAL_ERROR + status=unknownKesinlikle yeni key ile yeni SMS atmayın; aynı key/body ile retry edin.

Idempotency önerilen endpointler: /send, /otp/create, /otp/check, /hosted/create. /report, /balance, /usage için gerekli değildir.

CAPACITY & CREDIT

Request, Unit ve SMS Credit birbirinden farklıdır

Request/Unit sayaçları account + environment düzeyinde tüm API Key’ler arasında ortaktır. LIVE paket limitleri hesabınıza göre değişebilir; her zaman /api/usage ile güncel değeri okuyun.

TEST Sandbox doğrulanmış limitleri

PencereRequestUnit
Saniye1020
Dakika300600
Saat5.00010.000
Gün (Türkiye saati)50.000100.000

Mevcut doğrulanmış outbound Unit maliyetleri: OTP Messaging send = 1, OTP Verification create = 2, Hosted OTP create = 5. Paket/operasyon fiyatlandırması yönetim tarafından değiştirilebildiği için sözleşmenizde ve güncel kullanım ekranında gösterilen değerler esas alınır.

429 RATE_LIMIT_EXCEEDED

Request penceresi dolmuştur.

429 API_UNIT_LIMIT_EXCEEDED

Unit penceresi dolmuştur.

SECURITY_RATE_LIMITED ayrıca abuse/security throttle’dır; paket kapasitesi ile aynı şey değildir.

SMS Credit: SMS gönderimi öncesi kredi reserve edilir. Provider reject/timeout/unavailable durumunda kredi serbest bırakılır. Provider kabulünde kredi tüketilir. /balance cevabındaki reserved_sms_credits geçici rezervasyonu gösterir.
ENDPOINT REFERENCE

Endpointler ve çalışan kod örnekleri

Örnekler TEST yollarını kullanır. LIVE için yalnız /test/api değiştirin. C#, PHP, Python ve Node örneklerinde authentication dahil edilmiştir.

POST/api/send/test/send

OTP Messaging send

Caller-supplied verification_code is rendered through an APPROVED template allowed for this API Key. The key must include OTP_MESSAGING permission.

Request

{"phone":"900000000001","template_key":"activation","verification_code":"123456"}

Success response

{"success":true,"request_id":"req_example","status":"accepted","message_id":"msg_example"}

Errors / durumlar: 400 INVALID_REQUEST / INVALID_PHONE / UNSUPPORTED_CALLING_CODE / MESSAGE_TOO_LONG · 402 INSUFFICIENT_SMS_CREDIT · 403 SERVICE_MODE_NOT_ALLOWED / TEMPLATE_NOT_ALLOWED / TEMPLATE_DISABLED · 409 IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS · 429 RATE_LIMIT_EXCEEDED / API_UNIT_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · 422 PROVIDER_REJECTED (TEST/LIVE); TEST ayrıca PROVIDER_TIMEOUT/UNAVAILABLE · LIVE 503 PROVIDER_TIMEOUT / PROVIDER_UNAVAILABLE / GATEWAY_NOT_CONFIGURED · 500 INTERNAL_ERROR

curl
curl --fail-with-body -X POST "${BASE_URL}/test/send" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-unique-001" \
  --data '{"phone":"900000000001","template_key":"activation","verification_code":"123456"}'
PHP
$ch = curl_init($baseUrl . '/test/send');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', 'Idempotency-Key: example-unique-001'],
  CURLOPT_POSTFIELDS => '{"phone":"900000000001","template_key":"activation","verification_code":"123456"}'
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.post(base_url + '/test/send', headers={'Authorization': 'Bearer ' + api_key, 'Idempotency-Key': 'example-unique-001'}, json={"phone":"900000000001","template_key":"activation","verification_code":"123456"}, timeout=10)
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/test/send");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.TryAddWithoutValidation("Idempotency-Key", "example-unique-001");
request.Content = new StringContent(@"{""phone"":""900000000001"",""template_key"":""activation"",""verification_code"":""123456""}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/send', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'example-unique-001' },
  body: JSON.stringify({"phone":"900000000001","template_key":"activation","verification_code":"123456"})
});
const data = await response.json();
Postman
POST {{baseUrl}}/test/send · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: POST
URL: {{$env.SMSGONDER_BASE_URL}}/test/send
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
Header: Idempotency-Key = {{$json.idempotency_key}}
Body Content Type: JSON
JSON Body: {"phone":"900000000001","template_key":"activation","verification_code":"123456"}
POST/api/report/test/report

Delivery Report

Reads current persisted delivery state and performs at most one provider/sandbox report query. Provider report unavailability can still return HTTP 200 with provider_report_available=false and report_error.

Request

{"message_id":"msg_example"}

Success response

{"success":true,"request_id":"req_example","message_id":"msg_example","status":"delivered","environment":"test","provider_report_available":true,"live":true,"status_updated_at":"2026-09-04T10:00:00Z","checked_at":"2026-09-04T10:00:01Z"}

Errors / durumlar: 403 REPORT_DISABLED · 404 MESSAGE_NOT_FOUND · 429 RATE_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · LIVE 503 GATEWAY_NOT_CONFIGURED · provider query timeout/unavailable normally appears inside HTTP 200 report_error

curl
curl --fail-with-body -X POST "${BASE_URL}/test/report" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  --data '{"message_id":"msg_example"}'
PHP
$ch = curl_init($baseUrl . '/test/report');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => '{"message_id":"msg_example"}'
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.post(base_url + '/test/report', headers={'Authorization': 'Bearer ' + api_key}, json={"message_id":"msg_example"}, timeout=10)
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/test/report");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Content = new StringContent(@"{""message_id"":""msg_example""}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/report', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"message_id":"msg_example"})
});
const data = await response.json();
Postman
POST {{baseUrl}}/test/report · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: POST
URL: {{$env.SMSGONDER_BASE_URL}}/test/report
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
Body Content Type: JSON
JSON Body: {"message_id":"msg_example"}
POST/api/otp/create/test/otp/create

OTP Verification create

Requires OTP_VERIFICATION permission. Generates a secure OTP using the API Key code policy, sends it once and returns opaque otp_id. The customer never receives the generated LIVE code from this endpoint.

Request

{"phone":"900000000001","template_key":"activation"}

Success response

{"success":true,"request_id":"req_example","status":"accepted","otp_id":"otp_example","message_id":"msg_example","expires_in":300,"max_attempts":5}

Errors / durumlar: 400 INVALID_REQUEST / INVALID_PHONE / UNSUPPORTED_CALLING_CODE / MESSAGE_TOO_LONG · 402 INSUFFICIENT_SMS_CREDIT · 403 SERVICE_MODE_NOT_ALLOWED / TEMPLATE_NOT_ALLOWED / TEMPLATE_DISABLED · 409 IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS · 429 RATE_LIMIT_EXCEEDED / API_UNIT_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · 422 PROVIDER_REJECTED (TEST/LIVE); TEST ayrıca PROVIDER_TIMEOUT/UNAVAILABLE · LIVE 503 PROVIDER_TIMEOUT / PROVIDER_UNAVAILABLE / GATEWAY_NOT_CONFIGURED · 500 INTERNAL_ERROR

curl
curl --fail-with-body -X POST "${BASE_URL}/test/otp/create" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-unique-001" \
  --data '{"phone":"900000000001","template_key":"activation"}'
PHP
$ch = curl_init($baseUrl . '/test/otp/create');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', 'Idempotency-Key: example-unique-001'],
  CURLOPT_POSTFIELDS => '{"phone":"900000000001","template_key":"activation"}'
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.post(base_url + '/test/otp/create', headers={'Authorization': 'Bearer ' + api_key, 'Idempotency-Key': 'example-unique-001'}, json={"phone":"900000000001","template_key":"activation"}, timeout=10)
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/test/otp/create");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.TryAddWithoutValidation("Idempotency-Key", "example-unique-001");
request.Content = new StringContent(@"{""phone"":""900000000001"",""template_key"":""activation""}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/otp/create', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'example-unique-001' },
  body: JSON.stringify({"phone":"900000000001","template_key":"activation"})
});
const data = await response.json();
Postman
POST {{baseUrl}}/test/otp/create · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: POST
URL: {{$env.SMSGONDER_BASE_URL}}/test/otp/create
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
Header: Idempotency-Key = {{$json.idempotency_key}}
Body Content Type: JSON
JSON Body: {"phone":"900000000001","template_key":"activation"}
POST/api/otp/check/test/otp/check

OTP Verification check

Checks one otp_id using its immutable code-policy snapshot. verified/invalid/locked/expired are HTTP 200 states. A previously verified OTP returns verified idempotently.

Request

{"otp_id":"otp_example","verification_code":"123456"}

Success response

{"success":true,"request_id":"req_example","otp_id":"otp_example","status":"verified"}

Errors / durumlar: 400 INVALID_REQUEST · 404 OTP_NOT_FOUND · 409 IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_PROGRESS · 429 RATE_LIMIT_EXCEEDED / API_UNIT_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · 500 INTERNAL_ERROR. locked/expired are response status values, not HTTP errors.

curl
curl --fail-with-body -X POST "${BASE_URL}/test/otp/check" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-unique-001" \
  --data '{"otp_id":"otp_example","verification_code":"123456"}'
PHP
$ch = curl_init($baseUrl . '/test/otp/check');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', 'Idempotency-Key: example-unique-001'],
  CURLOPT_POSTFIELDS => '{"otp_id":"otp_example","verification_code":"123456"}'
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.post(base_url + '/test/otp/check', headers={'Authorization': 'Bearer ' + api_key, 'Idempotency-Key': 'example-unique-001'}, json={"otp_id":"otp_example","verification_code":"123456"}, timeout=10)
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/test/otp/check");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.TryAddWithoutValidation("Idempotency-Key", "example-unique-001");
request.Content = new StringContent(@"{""otp_id"":""otp_example"",""verification_code"":""123456""}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/otp/check', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'example-unique-001' },
  body: JSON.stringify({"otp_id":"otp_example","verification_code":"123456"})
});
const data = await response.json();
Postman
POST {{baseUrl}}/test/otp/check · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: POST
URL: {{$env.SMSGONDER_BASE_URL}}/test/otp/check
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
Header: Idempotency-Key = {{$json.idempotency_key}}
Body Content Type: JSON
JSON Body: {"otp_id":"otp_example","verification_code":"123456"}
POST/api/hosted/create/test/hosted/create

Hosted OTP create

Requires HOSTED_OTP permission. Creates a Hosted OTP session. return_url must exactly match the API Key allowlist; path, query and trailing slash are significant. Terminal result is posted back with HMAC signature.

Request

{"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"}

Success response

{"success":true,"request_id":"req_example","status":"accepted","transaction_id":"hotp_example","message_id":"msg_example","hosted_url":"https://www.smsgonder.com.tr/test/otp/token","expires_in":300}

Errors / durumlar: 400 INVALID_REQUEST / INVALID_RETURN_URL / INVALID_PHONE / UNSUPPORTED_CALLING_CODE / MESSAGE_TOO_LONG · 402 INSUFFICIENT_SMS_CREDIT · 403 SERVICE_MODE_NOT_ALLOWED / TEMPLATE_NOT_ALLOWED / TEMPLATE_DISABLED · 409 idempotency errors · 429 request/unit/security limits · 422 PROVIDER_REJECTED (TEST/LIVE); TEST ayrıca PROVIDER_TIMEOUT/UNAVAILABLE · LIVE 503 PROVIDER_TIMEOUT / PROVIDER_UNAVAILABLE / GATEWAY_NOT_CONFIGURED · 500 INTERNAL_ERROR

curl
curl --fail-with-body -X POST "${BASE_URL}/test/hosted/create" \
  -H "Authorization: Bearer ${API_KEY}" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: example-unique-001" \
  --data '{"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"}'
PHP
$ch = curl_init($baseUrl . '/test/hosted/create');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey, 'Content-Type: application/json', 'Idempotency-Key: example-unique-001'],
  CURLOPT_POSTFIELDS => '{"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"}'
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.post(base_url + '/test/hosted/create', headers={'Authorization': 'Bearer ' + api_key, 'Idempotency-Key': 'example-unique-001'}, json={"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"}, timeout=10)
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Post, baseUrl + "/test/hosted/create");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
request.Headers.TryAddWithoutValidation("Idempotency-Key", "example-unique-001");
request.Content = new StringContent(@"{""phone"":""900000000001"",""template_key"":""activation"",""return_url"":""https://client.example/otp-return"",""state"":""order-000001""}", Encoding.UTF8, "application/json");
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/hosted/create', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Idempotency-Key': 'example-unique-001' },
  body: JSON.stringify({"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"})
});
const data = await response.json();
Postman
POST {{baseUrl}}/test/hosted/create · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: POST
URL: {{$env.SMSGONDER_BASE_URL}}/test/hosted/create
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
Header: Idempotency-Key = {{$json.idempotency_key}}
Body Content Type: JSON
JSON Body: {"phone":"900000000001","template_key":"activation","return_url":"https://client.example/otp-return","state":"order-000001"}
GET/api/balance/test/balance

Balance

Reads LIVE SMS Credit or isolated TEST virtual credit. reserved_sms_credits is credit temporarily reserved by in-flight operations.

Success response

{"success":true,"request_id":"req_example","environment":"test","sms_credits":999,"reserved_sms_credits":0,"package":"Test Sandbox"}

Errors / durumlar: 401 INVALID_API_KEY · 403 API_KEY_DISABLED / ACCOUNT_SUSPENDED / ACCOUNT_CLOSED / SOURCE_IP_NOT_ALLOWED · 429 RATE_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · 500 INTERNAL_ERROR

curl
curl --fail-with-body \
  -H "Authorization: Bearer ${API_KEY}" \
  "${BASE_URL}/test/balance"
PHP
$ch = curl_init($baseUrl . '/test/balance');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey]]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.get(base_url + '/test/balance', headers={'Authorization': 'Bearer ' + api_key}, timeout=10)
response.raise_for_status()
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Get, baseUrl + "/test/balance");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/balance', {
  method: 'GET',
  headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
Postman
GET {{baseUrl}}/test/balance · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: GET
URL: {{$env.SMSGONDER_BASE_URL}}/test/balance
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
GET/api/usage/test/usage

Usage

Reads account-aggregate request and Unit counters for second, minute, hour and Turkey-time day. The call itself is metered as a request.

Success response

{"success":true,"request_id":"req_example","environment":"test","package":"Test Sandbox","units":{"second":{"used":0,"limit":20},"minute":{"used":0,"limit":600},"hour":{"used":0,"limit":10000},"day":{"used":0,"limit":100000}},"requests":{"second":{"used":1,"limit":10},"minute":{"used":1,"limit":300},"hour":{"used":1,"limit":5000},"day":{"used":1,"limit":50000}}}

Errors / durumlar: 401 INVALID_API_KEY · 403 API_KEY_DISABLED / ACCOUNT_SUSPENDED / ACCOUNT_CLOSED / SOURCE_IP_NOT_ALLOWED · 429 RATE_LIMIT_EXCEEDED / SECURITY_RATE_LIMITED · 500 INTERNAL_ERROR

curl
curl --fail-with-body \
  -H "Authorization: Bearer ${API_KEY}" \
  "${BASE_URL}/test/usage"
PHP
$ch = curl_init($baseUrl . '/test/usage');
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $apiKey]]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
Python
response = requests.get(base_url + '/test/usage', headers={'Authorization': 'Bearer ' + api_key}, timeout=10)
response.raise_for_status()
data = response.json()
C# (.NET)
using var request = new HttpRequestMessage(HttpMethod.Get, baseUrl + "/test/usage");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
using var response = await client.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
JavaScript / Node.js 18+
const response = await fetch(baseUrl + '/test/usage', {
  method: 'GET',
  headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
Postman
GET {{baseUrl}}/test/usage · Authorization: Bearer izinli API Key · templateKey=activation örneğini key için izin verilen template ile değiştirin.
n8n HTTP Request
HTTP Request node
Method: GET
URL: {{$env.SMSGONDER_BASE_URL}}/test/usage
Header: Authorization = Bearer {{$env.SMSGONDER_API_KEY}}
OTP VERIFICATION

/otp/check durumlarını HTTP hatası sanmayın

HTTPstatusAnlam
200verifiedKod doğrulandı. Daha sonra tekrar check edilirse yine verified döner.
200invalidAPI çağrısı başarılı fakat kod eşleşmedi; deneme sayısı ilerler.
200lockedMaksimum deneme sayısına ulaşıldı.
200expiredOTP TTL sona erdi.
404OTP_NOT_FOUND: otp_id bu key/environment için bulunamadı.
TEST SANDBOX

Deterministik telefon senaryoları

PhoneSonuçNot
900000000001accepted → deliveredNormal başarı akışı.
900000000002accepted → failedDelivery failure.
900000000003422 PROVIDER_REJECTEDGerçek provider yok; SMS Credit serbest bırakılır.
900000000004422 PROVIDER_TIMEOUTGerçek bekleme/retry yok; SMS Credit serbest bırakılır.
900000000005accepted → pendingReport pending kalır.

TEST numeric OTP: 12345678 değerinin key uzunluğu kadar öneki. Örnek: 6 hane → 123456. ALPHANUMERIC: A2B3C4D5 öneki.

DELIVERY REPORT

Provider report erişilemiyorsa API yine 200 dönebilir

{
  "success": true,
  "request_id": "req_example",
  "message_id": "msg_example",
  "status": "delivered",
  "environment": "test",
  "provider_report_available": true,
  "live": true,
  "status_updated_at": "2026-09-04T10:00:00Z",
  "checked_at": "2026-09-04T10:00:01Z"
}

environment gerçek LIVE/TEST ortamıdır. Deprecated live alanı ortam değildir; backward compatibility nedeniyle provider_report_available alias’ı olarak kalır.

Provider report kaynağı geçici erişilemezse top-level API çağrısı başarılı olabilir: provider_report_available=false ve report_error döner. Mevcut kayıtlı status yine response içinde kalır.

HOSTED OTP RETURN

Hosted sonucu imzalı form POST olarak döner

/hosted/create cevabındaki hosted_url kullanıcıya açılır. Kullanıcı verified/expired/locked terminal durumuna geldiğinde SMSGonder browser üzerinden sizin exact allowlisted return_url adresinize application/x-www-form-urlencoded form POST yapar.

AlanAçıklama
transaction_idHosted işlem kimliği.
resultverified, expired veya locked.
stateCreate request’inde verdiğiniz değer; aynen geri döner.
timestampUnix seconds.
signatureBase64 HMAC-SHA256.

Canonical signature input:

transaction_id + "\n" +
result         + "\n" +
state          + "\n" +
timestamp

signature = Base64(HMACSHA256(UTF8(signing_secret), UTF8(canonical)))

Python doğrulama
import base64, hashlib, hmac

canonical = f"{transaction_id}\n{result}\n{state}\n{timestamp}".encode("utf-8")
expected = base64.b64encode(
    hmac.new(signing_secret.encode("utf-8"), canonical, hashlib.sha256).digest()
).decode("ascii")

if not hmac.compare_digest(expected, signature):
    raise ValueError("invalid SMSGonder signature")
C# doğrulama
static bool FixedTimeEquals(byte[] a, byte[] b)
{
    if (a == null || b == null || a.Length != b.Length) return false;
    var diff = 0;
    for (var i = 0; i < a.Length; i++) diff |= a[i] ^ b[i];
    return diff == 0;
}

var canonical = transactionId + "\n" + result + "\n" + (state ?? "") + "\n" + timestamp;
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(signingSecret)))
{
    var expected = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical));
    var supplied = Convert.FromBase64String(signature);
    if (!FixedTimeEquals(expected, supplied))
        throw new InvalidOperationException("Invalid SMSGonder signature");
}
PHP doğrulama
$canonical = $transactionId . "\n" . $result . "\n" . $state . "\n" . $timestamp;
$expected = base64_encode(hash_hmac("sha256", $canonical, $signingSecret, true));

if (!hash_equals($expected, $signature)) {
    throw new RuntimeException("Invalid SMSGonder signature");
}
Node.js doğrulama
import crypto from "node:crypto";

const canonical = `${transactionId}\n${result}\n${state ?? ""}\n${timestamp}`;
const expected = crypto
  .createHmac("sha256", signingSecret)
  .update(canonical, "utf8")
  .digest("base64");

const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
  throw new Error("invalid SMSGonder signature");
}
Replay güvenliği: Aynı transaction_id sonucu kullanıcı tarafından yeniden POST edilebilir. İlk başarılı sonucu idempotent kaydedin; aynı transaction için farklı result/state/signature kabul etmeyin. Signing Secret yalnız oluşturulduğunda/yenilendiğinde gösterilir.

return_url exact eşleşir: scheme, host, path, query ve sondaki / önemlidir. Fragment yasaktır. Redirect URL gibi davranmaz; allowlist’te ne kayıtlıysa request’te o kullanılmalıdır.

WEBHOOK

Delivery status push entegrasyonu

Webhook API Key ayarından açılır. URL public HTTPS olmalı; redirect takip edilmez. Webhook sonucu delivery state değiştiğinde gönderilir.

JSON payload

{
  "environment": "live",
  "event": "sms.delivered",
  "event_id": "evt_example",
  "message_id": "msg_example",
  "status": "delivered",
  "status_updated_at": "2026-09-08T10:00:00.0000000Z"
}

Headers

X-SMSGonder-Event: sms.delivered
X-SMSGonder-Event-Id: evt_example
X-SMSGonder-Timestamp: 1788852000
X-SMSGonder-Signature: v1=<lowercase-hex-hmac>

Webhook signature: raw request body parse edilmeden önce byte-for-byte doğrulayın.

canonical_bytes = ASCII(timestamp + ".") + raw_body_bytes
signature = "v1=" + lowerhex(HMACSHA256(UTF8(signing_secret), canonical_bytes))
Node.js doğrulama
import crypto from "node:crypto";

const canonical = Buffer.concat([
  Buffer.from(timestamp + ".", "ascii"),
  rawBodyBuffer
]);
const expected = "v1=" + crypto
  .createHmac("sha256", signingSecret)
  .update(canonical)
  .digest("hex");

const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature, "utf8");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
  throw new Error("invalid SMSGonder webhook signature");
}
Webhook endpoint davranışı: hızlı bir 2xx dönün. SMSGonder redirect takip etmez. Başarısız webhook denemeleri sayılır; yapılandırılmış failure threshold aşılırsa webhook SUSPENDED olur ve Dashboard → API Keys üzerinden yeniden etkinleştirilir. Aynı event için otomatik sonsuz retry döngüsü yoktur; event_id ile idempotent işleyin. Webhook attempt’leri de account/environment kapasitesine tabidir; kapasite doluysa attempt SKIPPED_CAPACITY olarak atlanabilir.

TEST webhook testi: 900000000001/002 ile mesaj oluşturduktan sonra /test/report çağrısı status değişimini uygular ve webhook açıksa TEST event’i üretir.

ERROR HANDLING

HTTP status + stable error code

{
  "success": false,
  "request_id": "req_example",
  "code": "INVALID_REQUEST",
  "message": "The request is invalid."
}
HTTPBaşlıca code’larNe yapmalısınız?
400INVALID_REQUEST, INVALID_PHONE, UNSUPPORTED_CALLING_CODE, MESSAGE_TOO_LONG, INVALID_RETURN_URLRequest/config düzeltin; aynı hatayı otomatik retry etmeyin.
401INVALID_API_KEYAPI secret’ı kontrol edin.
402INSUFFICIENT_SMS_CREDITLIVE SMS Credit yükleyin.
403API_KEY_DISABLED, ACCOUNT_SUSPENDED, ACCOUNT_CLOSED, SOURCE_IP_NOT_ALLOWED, SERVICE_MODE_NOT_ALLOWED, TEMPLATE_NOT_ALLOWED, TEMPLATE_DISABLED, REPORT_DISABLEDDashboard/account ayarını düzeltin.
404MESSAGE_NOT_FOUND, OTP_NOT_FOUNDDoğru environment/key ve ID kullandığınızı kontrol edin.
409IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESSConflict’te yeni logical key; in-progress’te aynı key/body ile bekleyip retry.
422PROVIDER_REJECTED (TEST/LIVE); TEST ayrıca PROVIDER_TIMEOUT / PROVIDER_UNAVAILABLEProvider mesajı kabul etmedi veya deterministik Sandbox provider sonucu oluştu. SMS Credit serbest bırakılır; upstream_code varsa provider ayrıntısını taşır.
429RATE_LIMIT_EXCEEDED, API_UNIT_LIMIT_EXCEEDED, SECURITY_RATE_LIMITEDİlgili pencerenin resetini bekleyin; agresif retry yapmayın.
500INTERNAL_ERRORstatus=unknown ise aynı Idempotency-Key/body; aksi durumda request_id ile destek.
503GATEWAY_NOT_CONFIGURED, LIVE PROVIDER_TIMEOUT, LIVE PROVIDER_UNAVAILABLEGeçici servis/provider bağımlılığı sorunu. Yeni SMS oluşturmayın; aynı mantıksal işlem için aynı Idempotency-Key ve aynı body ile güvenli retry edin.
HTTP transport notu: SMSGonder kontrollü provider sonuçlarında origin 502/504 üretmez. Bazı reverse proxy/CDN katmanları bu status kodlarının JSON body’sini kendi gateway sayfasıyla değiştirebildiği için provider rejection 422, geçici provider timeout/unavailable ise 503 kullanır. Entegrasyonda karar verirken her zaman stable code alanını; teşhis için varsa upstream_code alanını kullanın.
CANLI KULLANIMA GEÇİŞ

Entegrasyonunuzu canlıya alın

TEST ortamında kendi akışlarınızı doğruladıktan sonra LIVE kullanıma geçebilirsiniz. Bir API Key aynı anda birden fazla Service Mode ve birden fazla onaylı Template için yetkilendirilebilir; her servis veya template için ayrı key oluşturmanız gerekmez.
  1. API Key secret ve varsa Signing Secret değerlerini yalnız sunucu tarafında saklayın; tarayıcı, mobil uygulama veya herkese açık kaynak kod içinde kullanmayın.
  2. Dashboard’daki API Key ayarlarından uygulamanızın kullanacağı Service Mode’ları seçin: OTP_MESSAGING, OTP_VERIFICATION ve/veya HOSTED_OTP.
  3. Kullanacağınız onaylı template’leri aynı API Key’e yetkilendirin. İsteklerde yalnız size tanımlı template_key değerlerini gönderin; request body içinde service_mode göndermeniz gerekmez.
  4. IP allowlist kullanıyorsanız uygulamanızın internete çıktığı sabit IP/CIDR adreslerini API Key ayarlarına ekleyin.
  5. Hosted OTP kullanıyorsanız canlı return_url adresinizi allowlist’e ekleyin ve dönüş imzasını Signing Secret ile doğrulayın.
  6. Webhook kullanıyorsanız HTTPS endpoint’inizi tanımlayın, gelen imzayı doğrulayın ve aynı event_id tekrar geldiğinde işlemi ikinci kez uygulamayın.
  7. LIVE SMS gönderecekseniz hesabınızda yeterli SMS Credit bulunduğunu ve kullanım planınızın ihtiyacınız olan API kapasitesini karşıladığını kontrol edin.
  8. Canlıya geçerken endpoint yollarını /test/* yerine /api/* olarak değiştirin. TEST ortamında oluşan ID’leri LIVE işlemlerinde kullanmayın.
  9. Uygulama loglarında sorun takibi için request_id, message_id, otp_id ve varsa transaction_id değerlerini saklayın.
  10. Aynı işlemi yeniden denemeniz gerekirse yeni SMS oluşturmamak için aynı Idempotency-Key ve aynı request body ile tekrar gönderin.