Genel bakış
- Tüm istekler HTTPS ve JSON’dur; gövdeli isteklerde Content-Type: application/json gönderin.
- Tarihler UTC ve Z ile yazılır (2026-10-06T09:00:00Z). Tutarlar 8 ondalıklı metin olarak döner ("0.25000000").
- Başarılı yanıtlar data ve meta.request_id alanlarını, hatalar error ve meta.request_id alanlarını içerir.
- Her yanıtta X-Request-Id başlığı vardır; destek taleplerinde bu kimliği paylaşın. Kendi X-Request-Id değerinizi de gönderebilirsiniz.
- Hata mesajlarının dili Accept-Language ile seçilir (tr, en); hata kodları sabittir, metni değil kodu kontrol edin.
- 202 Accepted, mesajın kalıcı olarak kabul edildiği anlamına gelir; teslim sonucu durum sorgusu veya webhook ile gelir.
| Uç nokta | Yetki | Başarı | Açıklama |
|---|---|---|---|
POST/messages |
messages:write | 202 | Tek alıcıya mesaj gönderir. |
POST/message-batches |
messages:write | 202 | En fazla 100 mesajı tek istekte, bütünüyle kabul eder veya reddeder. |
GET/messages/{id} |
messages:read | 200 | Bir mesajın durumunu, ücretini veya kredisini döndürür. |
GET/messages |
messages:read | 200 | Mesajları filtreleyip sayfalı listeler. |
GET/message-batches/{id} |
messages:read | 200 | Toplu gönderimin durum dağılımını ve mesajlarını döndürür. |
POST/messages/{id}/cancel |
messages:write | 200 | Kuyrukta bekleyen (ör. planlı) mesajı iptal eder. |
GET/wallet |
wallet:read | 200 | Bakiye, ödeme modeli ve kalan SMS kredisini döndürür. |
Kimlik doğrulama
API anahtarını panelde Hesap → API anahtarları sayfasından oluşturun. Anahtar (smsgo_live_ ile başlar) yalnız bir kez gösterilir; sunucu tarafında gizli tutun, istemci uygulamalarına koymayın. Her istekte Authorization başlığıyla gönderin:
curl https://smsgoo.com/api/v1/wallet \
-H "Authorization: Bearer smsgo_live_XXXXXXXX"
Yetkiler (scope)
messages:write— Mesaj ve toplu gönderim, iptalmessages:read— Mesaj ve toplu gönderim sorgulama, listelemewallet:read— Bakiye ve kredi sorgulama
Güvenlik
- Anahtara en fazla 20 IP adresi veya CIDR aralığı tanımlayarak yalnız sunucularınızdan kullanılmasını sağlayın (diğer adresler 403 ip_not_allowed).
- Anahtarlara son kullanma tarihi verebilir, sızan anahtarı tek tıkla iptal edebilirsiniz.
- Her ortam/uygulama için ayrı anahtar ve yalnız gereken yetkileri kullanın.
Alıcı numaraları
Yalnız Türkiye GSM abone numaralarına gönderim yapılır. API’de to alanını E.164 olarak yazın: +90 ve 10 hane (+905321112233). Tahsisli olmayan kodlar, sabit hatlar, 850/444/800 numaraları ve yurt dışı numaraları gönderimden önce reddedilir; yanlış numaraya kredi veya bakiye harcanmaz.
- Kabul edilen
- +90 501, 505, 506, 507, 510, 516, 530–539, 540–549, 551–555, 559, 561
- Panel ve listelerde ayrıca
- 05321112233 · 5321112233 · 905321112233 · 00905321112233 · +90 (532) 111 22 33
Reddedilen numara 422 validation_failed döner; gerekçe error.fields.to (toplu gönderimde error.fields["messages.N.to"]) içindedir:
| Gerekçe | Örnek |
|---|---|
| Numara biçimi hatalı | abc, 0532-ABC |
| Hane sayısı hatalı | 053211122, 4441234 |
| Türkiye dışı numara | +4915112345678 |
| Sabit hat numarası | 02121112233 |
| Özel/kurumsal numara (850, 444, 800…) | 08501112233 |
| Tahsisli olmayan GSM kodu | +905701112233, +905821112233 |
Panelden toplu gönderimde liste önce temizlenir: kaç numara olduğu, kaçının hangi gerekçeyle ayıklandığı ve kaç tekrarın çıkarıldığı gönderimden önce gösterilir ve gönderim geçmişinde saklanır.
POST/messages
Tek alıcıya mesaj gönderir. Mesaj doğrulanır, ücreti ayrılır (veya SMS kredisi düşülür) ve kuyruğa alınır. Location başlığı mesajın sorgu adresini verir.
| Alan | Tür | Zorunlu | Açıklama |
|---|---|---|---|
| channel | string | Evet | sms (WhatsApp hesabınızda açıksa whatsapp). |
| to | string | Evet | E.164 Türkiye GSM numarası, ör. +905321112233. |
| sender_id | string | Hayır | Panelde onaylanmış gönderici (başlık) kimliği. SMS için verilmezse mesaj başlıksız gönderilir; WhatsApp için zorunludur. |
| content.type | string | Evet | SMS için text. |
| content.text | string | Evet | Mesaj metni, en fazla 5 SMS parçası: Türkçe karakter yoksa 765, varsa 335 karakter. |
| client_reference | string | Hayır | Kendi referansınız (1–100 karakter); listede filtrelenebilir, webhook ile geri gelir. |
| schedule_at | string | Hayır | İleri tarihli gönderim, UTC ve Z ile: 2026-10-07T09:00:00Z (en fazla 30 gün sonrası). |
| expires_at | string | Hayır | Bu andan sonra gönderilmez. Varsayılan gönderimden 24 saat sonra, en fazla 7 gün. |
Bilinmeyen alanlar reddedilir (422). Türkçe karakter içeren metin Unicode kodlanır ve boy sayısı artar; 1 boy GSM metinde 160, Unicode metinde 70 karakterdir, çok boylu mesajlarda bu sınırlar 153 ve 67’dir.
curl -X POST https://smsgoo.com/api/v1/messages \
-H "Authorization: Bearer smsgo_live_XXXXXXXX" \
-H "Idempotency-Key: order-1042-sms-0001" \
-H "Content-Type: application/json" \
-H "Accept-Language: tr" \
-d '{
"channel": "sms",
"to": "+905321112233",
"sender_id": "01SENDEREXAMPLE",
"content": {"type": "text", "text": "Siparişiniz yola çıktı."},
"client_reference": "order-1042"
}'
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('SMSGO_API_KEY'))
->withHeaders(['Idempotency-Key' => 'order-1042-sms-0001'])
->acceptJson()
->post('https://smsgoo.com/api/v1/messages', [
'channel' => 'sms',
'to' => '+905321112233',
'sender_id' => '01SENDEREXAMPLE',
'content' => ['type' => 'text', 'text' => 'Siparişiniz yola çıktı.'],
'client_reference' => 'order-1042',
]);
if ($response->status() === 202) {
$messageId = $response->json('data.id');
}
const response = await fetch('https://smsgoo.com/api/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SMSGO_API_KEY}`,
'Idempotency-Key': 'order-1042-sms-0001',
'Content-Type': 'application/json',
},
body: JSON.stringify({
channel: 'sms',
to: '+905321112233',
sender_id: '01SENDEREXAMPLE',
content: { type: 'text', text: 'Siparişiniz yola çıktı.' },
client_reference: 'order-1042',
}),
});
const { data, error } = await response.json();
import os, requests
response = requests.post(
"https://smsgoo.com/api/v1/messages",
headers={
"Authorization": f"Bearer {os.environ['SMSGO_API_KEY']}",
"Idempotency-Key": "order-1042-sms-0001",
},
json={
"channel": "sms",
"to": "+905321112233",
"sender_id": "01SENDEREXAMPLE",
"content": {"type": "text", "text": "Siparişiniz yola çıktı."},
"client_reference": "order-1042",
},
timeout=10,
)
message_id = response.json()["data"]["id"] if response.status_code == 202 else None
{
"data": {
"id": "01J9Z8Q4M6T2V3X5Y7A9B1C3D5",
"channel": "sms",
"status": "queued",
"billing_status": "reserved",
"reserved_amount": "0.25000000",
"currency": "TRY",
"credits": null,
"created_at": "2026-10-06T09:00:00Z"
},
"meta": {"request_id": "01J9Z8Q4M6REQUEST0000000000"}
}
POST/message-batches
Tek istekte 1–100 mesaj gönderir. Batch bütündür: bir öğe hatalıysa hiçbir mesaj oluşturulmaz ve hata, öğenin yolu (messages.N.alan) ile döner. Bakiye/kredi de tüm batch için tek seferde ayrılır. Büyük listeleri 100’lük parçalara bölüp her parça için ayrı Idempotency-Key kullanın; hatalı numaraları göndermeden önce ayıklamanız parçaların reddedilmesini önler.
curl -X POST https://smsgoo.com/api/v1/message-batches \
-H "Authorization: Bearer smsgo_live_XXXXXXXX" \
-H "Idempotency-Key: campaign-77-part-001" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"channel": "sms", "to": "+905321112233", "sender_id": "01SENDEREXAMPLE",
"content": {"type": "text", "text": "Kampanya başladı!"}, "client_reference": "c77-1"},
{"channel": "sms", "to": "+905421112233", "sender_id": "01SENDEREXAMPLE",
"content": {"type": "text", "text": "Kampanya başladı!"}, "client_reference": "c77-2"}
]
}'
Aynı metni çok sayıda alıcıya gönderiyorsanız bu uç noktayı kullanın: aynı metin ve göndericili mesajlar operatöre toplu iletilir, bu yüzden tek tek /messages çağrılarından çok daha hızlı gider.
Yanıt (202): id, status=accepted, message_ids (gönderim sırasıyla), reserved_amount, currency, created_at. Bir öğe hatalıysa:
{
"error": {
"code": "validation_failed",
"message": "İstek alanlarını kontrol edin.",
"fields": {
"messages.1.to": ["Bu GSM alan kodu Türkiye’de tahsisli değil."]
}
},
"meta": {"request_id": "01J9Z8Q4M6REQUEST0000000001"}
}
Mesaj sorgulama
GET/messages/{id}
Mesajın güncel durumu. Güvenlik gereği mesaj metni ve alıcı numarası API yanıtlarında yer almaz; kendi kaydınızı client_reference ile eşleyin.
{
"data": {
"id": "01J9Z8Q4M6T2V3X5Y7A9B1C3D5",
"channel": "sms",
"status": "delivered",
"billing_status": "captured",
"client_reference": "order-1042",
"reserved_amount": "0.25000000",
"charged_amount": "0.25000000",
"currency": "TRY",
"credits": null,
"schedule_at": "2026-10-06T09:00:00Z",
"expires_at": "2026-10-07T09:00:00Z",
"failure_code": null,
"created_at": "2026-10-06T09:00:00Z",
"updated_at": "2026-10-06T09:00:04Z"
},
"meta": {"request_id": "01J9Z8Q4M6REQUEST0000000002"}
}
GET/messages
Filtreler: status, channel, created_from, created_to (Z ile UTC), client_reference, limit (1–100, varsayılan 25). Sonuç en yeniden eskiye sıralıdır. Sonraki sayfa için meta.next_cursor değerini aynı filtrelerle cursor parametresinde gönderin; son sayfada next_cursor null olur. İmleç başka filtreyle kullanılırsa 422 invalid_cursor döner.
# İlk sayfa: dünün teslim edilemeyen mesajları
curl "https://smsgoo.com/api/v1/messages?status=undeliverable&created_from=2026-10-05T00:00:00Z&created_to=2026-10-06T00:00:00Z&limit=100" \
-H "Authorization: Bearer smsgo_live_XXXXXXXX"
# Sonraki sayfa: aynı filtreler + meta.next_cursor
curl "https://smsgoo.com/api/v1/messages?status=undeliverable&created_from=2026-10-05T00:00:00Z&created_to=2026-10-06T00:00:00Z&limit=100&cursor=eyJjIjoi..." \
-H "Authorization: Bearer smsgo_live_XXXXXXXX"
GET/message-batches/{id}
id, total, counts_by_status (ör. {"delivered": 97, "undeliverable": 3}) ve batch mesajlarının bir sayfası (messages) döner; meta.next_cursor ve limit/cursor parametreleri listeleme ile aynıdır.
POST/messages/{id}/cancel
Yalnız status=queued iken (çoğunlukla ileri tarihli mesajlar) iptal edilir; ayrılan tutar veya kredi iade edilir. Gövde gönderilmez (veya {}). Zaten iptal edilmiş mesaj için 200 ile aynı kaynak döner; gönderime girmiş mesaj için 409 message_not_cancelable. Idempotency-Key zorunludur.
GET/wallet
Hesabınız iki ödeme modelinden biriyle çalışır; billing_mode alanı hangisi olduğunu söyler:
- wallet — Gönderimler tarifenize göre para bakiyesinden ödenir. reserved_amount ve charged_amount mesaj ücretini gösterir; yetersiz bakiye 409 insufficient_balance.
- credits — Gönderimler SMS kredisinden düşer: 1 kredi = 1 SMS; uzun mesajlar birden fazla SMS sayılır. Mesajın credits alanı düşülen krediyi gösterir; yetersiz kredi 409 insufficient_credits. Yalnız siz iptal ettiğiniz (henüz gönderilmemiş) mesajın kredisi iade edilir; başarısız, teslim edilemeyen veya süresi dolan mesajın kredisi iade edilmez.
{
"data": {
"currency": "TRY",
"posted_balance": "0.00000000",
"reserved_amount": "0.00000000",
"available_balance": "0.00000000",
"billing_mode": "credits",
"credits": 9842,
"as_of": "2026-10-06T09:00:00Z"
},
"meta": {"request_id": "01J9Z8Q4M6REQUEST0000000003"}
}
Durumlar ve hata kodları
| status | Anlamı |
|---|---|
| queued | Kabul edildi, gönderim sırasını veya planlanan saati bekliyor. Yalnız bu durumda iptal edilebilir. |
| submitting | Operatöre iletiliyor. |
| accepted | Operatör aldı; teslim raporu bekleniyor. |
| unknown | İletim sonucu henüz doğrulanamadı; mutabakat sonrası güncellenir. |
| delivered | Alıcıya teslim edildi. |
| failed | Operatöre iletilemedi. |
| undeliverable | Operatör alıcıya ulaşamadı (kapalı hat, geçersiz abone vb.). |
| expired | Geçerlilik süresi teslimden önce doldu. |
| canceled | Sizin tarafınızdan iptal edildi. |
| billing_status | Anlamı |
|---|---|
| reserved | Tutar veya kredi ayrıldı. |
| captured | Ücret kesinleşti. |
| released | İptal edilen mesajın ayrılan tutarı/kredisi iade edildi. |
| adjusted | Mutabakat sonrası düzeltildi. |
| failure_code | Anlamı |
|---|---|
| send_failed | Operatöre iletilemedi. Ücret iade edilmez. |
| undeliverable | Operatör alıcıya ulaşamadı. |
| expired | Geçerlilik süresi doldu. |
Nihai durumlar delivered, failed, undeliverable, expired ve canceled’dır. Operatör raporu geç gelirse nihai bir durum sonradan düzeltilebilir (örneğin undeliverable → delivered); her değişiklik sequence değerini artırır, en yüksek sequence geçerlidir. Webhook kullanmıyorsanız mesajı birkaç dakika arayla sorgulayın.
Idempotency
Tüm POST isteklerinde Idempotency-Key başlığı zorunludur: 16–128 karakter, yalnız harf, rakam ve . _ : - karakterleri. Aynı anahtar ve aynı gövdeyle tekrar edilen istek yeni mesaj oluşturmaz; ilk yanıt Idempotency-Replayed: true başlığıyla aynen döner. Böylece ağ hatasında güvenle yeniden deneyebilirsiniz.
- Anahtarı iş kaydınızdan türetin (ör. order-1042-sms-0001); her yeni mesaj veya batch için yeni anahtar kullanın.
- Aynı anahtar farklı gövdeyle gelirse 409 idempotency_conflict; ilk istek hâlâ işleniyorsa 409 request_in_progress (Retry-After: 1).
- Hata ile sonuçlanan istekte anahtar serbest kalır; düzeltilmiş istekle aynı anahtarı kullanabilirsiniz. Başarılı yanıtlar 30 gün saklanır.
Limitler
- Hesap başına saniyede 10 POST ve 30 GET isteği; anahtar çoğaltmak limiti artırmaz. Hesabınıza ayrıca dakikalık, günlük veya aylık gönderim sınırı tanımlanmış olabilir.
- Sınır aşıldığında 429 rate_limited ve Retry-After (saniye) döner; bu süre kadar bekleyip aynı Idempotency-Key ile tekrar deneyin.
- İstek gövdesi en fazla 1 MB; batch başına en fazla 100 mesaj; mesaj metni en fazla 5.000 karakter.
- Yüksek hacim için 100’lük batch’leri sırayla veya düşük paralellikle gönderin; 429 aldığınızda geri çekilin.
Hata yanıtları
Hata gövdesi her zaman aynıdır: error.code (sabit), error.message (Accept-Language diline göre), alan hatalarında error.fields. Yeniden deneme kararını HTTP koduna ve error.code’a göre verin: 4xx hatalarında isteği düzeltin; 429 ve 503’te Retry-After kadar bekleyin; 500’de aynı Idempotency-Key ile sınırlı sayıda tekrar deneyin.
| HTTP | error.code | Anlamı |
|---|---|---|
| 400 | malformed_json | Gövde geçerli JSON değil. |
| 400 | invalid_idempotency_key | Idempotency-Key eksik veya biçimi hatalı. |
| 401 | unauthenticated | Anahtar yok, geçersiz, iptal edilmiş veya süresi dolmuş. |
| 403 | insufficient_scope | Anahtarın bu işlem için yetkisi yok. |
| 403 | ip_not_allowed | İstek, anahtarın izinli IP listesinde olmayan bir adresten geldi. |
| 403 | account_suspended | Hesap askıda. |
| 404 | resource_not_found | Kaynak yok veya başka bir hesaba ait. |
| 405 | method_not_allowed | Bu yol bu HTTP metodunu desteklemiyor. |
| 409 | insufficient_balance | Bakiye bu gönderimi karşılamıyor. |
| 409 | insufficient_credits | SMS kredisi bu gönderimi karşılamıyor. |
| 409 | idempotency_conflict | Aynı Idempotency-Key farklı bir gövdeyle kullanılmış. |
| 409 | request_in_progress | Aynı anahtarla bir istek hâlâ işleniyor; Retry-After kadar bekleyin. |
| 409 | message_not_cancelable | Mesaj artık kuyrukta değil. |
| 413 | payload_too_large | Gövde 1 MB sınırını aşıyor. |
| 415 | unsupported_media_type | Content-Type application/json olmalı. |
| 422 | validation_failed | Alan hatası; ayrıntı error.fields içinde. |
| 422 | invalid_cursor | Sayfalama imleci bu hesap/filtre kümesine ait değil. |
| 422 | channel_unavailable | Kanal bu hedef için kullanılamıyor. |
| 422 | pricing_unavailable | Bu gönderim için geçerli tarife yok. |
| 429 | rate_limited | İstek veya gönderim sınırı aşıldı; Retry-After kadar bekleyin. |
| 503 | service_paused | Gönderim platform tarafından geçici olarak durduruldu; Retry-After sonra aynı anahtarla tekrar deneyin. |
| 503 | maintenance | Bakım çalışması. |
| 500 | internal_error | Beklenmeyen hata; request_id ile destek alın. |
Webhook
Panelde Hesap → Webhook sayfasından HTTPS (443) adresinizi tanımlayın; imza anahtarı (whsec_…) yalnız bir kez gösterilir. Mesaj durumu veya ücret durumu değiştiğinde adresinize JSON POST gönderilir. Bağlantıyı test etmek için panelden webhook.test olayı gönderebilirsiniz.
message.status_changed— status değişti.message.billing_updated— billing_status değişti (ücret kesinleşti veya iptal edilen mesajın tutarı iade edildi).webhook.test— Panelden gönderilen deneme; data yalnız {"test": true}.
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: SMSGO-Webhooks/1
X-SMSGO-Event-Id: 01J9Z9A0EVENT00000000000000
X-SMSGO-Timestamp: 1791277204
X-SMSGO-Signature: v1=5d41402abc4b2a76b9719d911017c592...
{
"id": "01J9Z9A0EVENT00000000000000",
"type": "message.status_changed",
"schema_version": "1",
"occurred_at": "2026-10-06T09:00:04Z",
"data": {
"message_id": "01J9Z8Q4M6T2V3X5Y7A9B1C3D5",
"status": "delivered",
"billing_status": "captured",
"sequence": 4,
"client_reference": "order-1042"
}
}
İmza doğrulama
X-SMSGO-Signature = "v1=" + HMAC-SHA256(imza anahtarı, X-SMSGO-Timestamp + "." + ham gövde) (küçük harf hex). Gövdeyi JSON olarak yeniden üretmeden ham baytlarla doğrulayın, sabit zamanlı karşılaştırma kullanın ve 300 saniyeden eski zaman damgalarını reddedin.
// Laravel controller: verify before trusting the body.
$secret = config('services.smsgo.webhook_secret'); // whsec_...
$timestamp = $request->header('X-SMSGO-Timestamp');
$signature = $request->header('X-SMSGO-Signature'); // v1=<hex>
$raw = $request->getContent(); // raw bytes, not re-encoded JSON
$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$raw, $secret);
abort_unless(
$timestamp && abs(time() - (int) $timestamp) <= 300 && hash_equals($expected, (string) $signature),
400
);
$event = json_decode($raw, true);
// Deduplicate on $event['id'], then answer 2xx quickly.
import crypto from 'node:crypto';
import express from 'express';
app.post('/smsgo/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-SMSGO-Timestamp');
const signature = req.get('X-SMSGO-Signature') ?? '';
const expected = 'v1=' + crypto
.createHmac('sha256', process.env.SMSGO_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = signature.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(400);
const event = JSON.parse(req.body);
// Deduplicate on event.id, then answer 2xx quickly.
res.sendStatus(204);
});
import hashlib, hmac, os, time
def verify(headers, raw_body: bytes) -> bool:
timestamp = headers.get("X-SMSGO-Timestamp", "")
signature = headers.get("X-SMSGO-Signature", "")
expected = "v1=" + hmac.new(
os.environ["SMSGO_WEBHOOK_SECRET"].encode(),
timestamp.encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
fresh = timestamp.isdigit() and abs(time.time() - int(timestamp)) <= 300
return fresh and hmac.compare_digest(expected, signature)
Teslim ve yeniden deneme
- 10 saniye içinde 2xx dönün; ağır işleri kuyruğa alın. Yönlendirmeler (3xx) izlenmez.
- 408, 429, 5xx ve bağlantı hatalarında 1 dk, 5 dk, 15 dk, 1 sa, 6 sa ve 12 sa sonra tekrar denenir (429’da Retry-After dikkate alınır); ilk denemeden 24 saat sonra bırakılır ve hesap sahibine e-posta gönderilir. Diğer 4xx yanıtlar kalıcı hata sayılır.
- Aynı olay birden fazla gelebilir ve sıra garanti değildir: id ile tekrarları ayıklayın, güncelliği data.sequence ile belirleyin. Başarısız teslimleri panelden yeniden gönderebilirsiniz.
- İmza anahtarını yenilediğinizde imzalama hemen yeni anahtara geçer; geçiş sırasında 24 saat boyunca eski ve yeni anahtarı birlikte kabul edin.
OpenAPI
Makine tarafından okunabilir OpenAPI 3.1 tanımını indirip Postman, Insomnia veya kod üreticilere aktarabilirsiniz. Sürüm v1 içinde yalnız geriye uyumlu (yeni isteğe bağlı alan gibi) değişiklikler yapılır; bilinmeyen alanları yok sayın.
openapi.json