# OTP SMS gönderme (Sandbox)

> Tek bir alıcıya OTP (tek kullanımlık şifre) SMS'i gönderir.

- **İstek:** `POST https://ext-sandbox.netgsm.com.tr/sms/v3/send/otp`
- **Sandbox:** `https://ext-sandbox.netgsm.com.tr/sms/v3`
- **Kimlik doğrulama:** `Authorization: Bearer <token>`
- **HTML sayfası:** https://api-docs.netgsm.com.tr/sandbox/send-otp/
- **OpenAPI tanımı:** https://api-docs.netgsm.com.tr/openapi/sandbox-v3.yaml (operationId: `sendOtp`)

## Açıklama

Tek bir alıcıya OTP (tek kullanımlık şifre) SMS'i gönderir. OTP, toplu SMS'ten ayrı bir kanaldan
anında gönderilir; her istek tek bir mesaj ve tek bir alıcıdır. 202 döndüğünde mesaj operatöre
iletilmiştir, teslim sonradan olur.

**Değerlerin işlenmesi.** Numara, gönderici ve metin gönderimden önce şu şekilde işlenir:

| Alan | İşlem |
|---|---|
| `recipient` | `5XXXXXXXXX`, `05XXXXXXXXX` ya da `905XXXXXXXXX` biçiminde; başına `+` eklenebilir. |
| `sender` | Büyük/küçük harf fark etmez. Abone numarası `5XXXXXXXXX`, `05XXXXXXXXX` ya da `905XXXXXXXXX` yazılabilir. |
| `text` | Baştaki ve sondaki boşluklar kırpılır; karakterler aşağıdaki tabloya göre çevrilir. Uzunluk çevrilmiş metin üzerinden sayılır. |

**Yeniden deneme.** 503 hiçbir şeyin gönderilmediğini gösterir; yeniden denemek güvenlidir.

**Raporlar.** OTP iletim raporları toplu gönderim raporlarından ayrı tutulur; `jobId`
`GET /report` ile sorgulanamaz.

## İstek

### Parametreler

- `x-client` (başlık, string, isteğe bağlı): Kurumsal OTP hizmeti tanımlı hesaplar `enterprise` göndermelidir (küçük harfle, yazıldığı gibi). Diğer hesaplar bu başlığı göndermez.

### Gövde (`application/json`, zorunlu)

- `sender` (string, zorunlu, en az uzunluk 1): Gönderici başlığı: hesaba kayıtlı bir alfanümerik başlık (örneğin `NETGSM`) ya da hesabın abone numarası. Büyük/küçük harf fark etmez. Abone numarası `5XXXXXXXXX`, `05XXXXXXXXX` ya da `905XXXXXXXXX` yazılabilir. Kayıtlı başlıkları `GET /senders` ile görebilirsiniz. Örnek: `NETGSM`.
- `text` (string, zorunlu, en az uzunluk 1): OTP metni. Baştaki ve sondaki boşluklar kırpılır, karakterler gönderimden önce çevrilir: Örnek: `Dogrulama kodunuz: 123456`.

  | Metinde | Gönderilen |
  |---|---|
  | `ç ğ ı ö ş ü Ç Ğ İ Ö Ş Ü` | `c g i o s u C G I O S U` |
  | `{ [` / `} ]` | `(` / `)` |
  | `“` `”` `` ` `` | `"` |
  | `‘ ’` | `'` |
  | `~` | `?` |
  | `€` | `;EO;` |
  | sekme, `\|` | boşluk |
  | `^` | silinir |
  | satır sonu | `\n` (iki karakter) |
  | diğer desteklenmeyen karakterler | `?` |

  OTP her zaman tek SMS'tir. Çevrilmiş metin alfanümerik başlıkta en fazla 155, abone numarasında
  en fazla 160 karakter olabilir; alfanümerik başlıkta sınır daha kısadır, çünkü operatör kodu
  metnin sonuna eklenir.

- `recipient` (string, zorunlu, en az uzunluk 1): Alıcı, bir Türkiye cep numarası: `5XXXXXXXXX`, `05XXXXXXXXX` ya da `905XXXXXXXXX`; başına `+` eklenebilir (`+905XXXXXXXXX`). Boşluk ya da yurt dışı numarası kabul edilmez. Örnek: `905061234567`.
- `appName` (string, isteğe bağlı): Gönderimi yapan uygulamanın adı. Yalnızca kayıt amaçlıdır.

Örnek gövde:

```json
{
  "sender": "NETGSM",
  "text": "Dogrulama kodunuz: 123456",
  "recipient": "905061234567",
  "appName": "shop"
}
```

## Örnek istek

```bash
curl -X POST 'https://ext-sandbox.netgsm.com.tr/sms/v3/send/otp' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "sender": "NETGSM",
  "text": "Dogrulama kodunuz: 123456",
  "recipient": "905061234567",
  "appName": "shop"
}'
```

## Yanıtlar

### 202 — OTP operatöre iletildi.

Gövde (`application/json`):

- `jobId` (string, zorunlu): OTP gönderiminin kimliği. Bir sorun bildirirken paylaşmalısınız. Örnek: `175518151237512050610525430`.

Örnek:

```json
{
  "jobId": "175518151237512050610525430"
}
```

### 400 — INVALID_REQUEST. `message`:

| `message` | Ne zaman |
|---|---|
| `N field(s) rejected` | `sender`, `text` ya da `recipient` eksik veya boş. `violations` belirtir. |
| `request body could not be read as json` | Gövde JSON değil. |
| `recipient is not a valid number` | `recipient` bir Türkiye cep numarası değil. |
| `text is longer than 155 characters for this sender` | Çevrilmiş metin alfanümerik başlıkta 155, abone numarasında 160 karakteri aşıyor (`160` yazar). |

Gövde (`application/json`):

- `code` (string, zorunlu): Değerler: `INVALID_REQUEST`.
- `message` (string, zorunlu): Okunabilir ayrıntı. `UNAUTHENTICATED` için `no bearer token` ya da `invalid or expired token`. `REJECTED_HEADER` için ret adıdır; yalnızca rakamlardan oluşan bir göndericide ` (subscriber number headers were not offered)` ekini taşıyabilir. `RATE_LIMITED` için sınırı belirtir; `FORBIDDEN` ve `REJECTED_IYS` için sabit bir cümledir. `SERVICE_UNAVAILABLE` ve `INTERNAL_ERROR` genel bir cümle taşır; bildirirken `requestId` değerini paylaşmanız gerekir.
- `requestId` (string, zorunlu): İsteğe özgü kimlik, 32 onaltılık karakter. Bir sorun bildirirken bu değeri paylaşmanız, isteğin kayıtlarda bulunmasını sağlar.
- `timestamp` (string, date-time, zorunlu)
- `violations` (array<object>, null olabilir, zorunlu): Yalnızca gövde doğrulamasından gelen INVALID_REQUEST için dolu, aksi halde `null`.
- `violations[].field` (string, isteğe bağlı)
- `violations[].message` (string, isteğe bağlı)
- `rejectedItems` (array<object>, isteğe bağlı, en fazla kayıt 100): Yalnızca `NO_DELIVERABLE_MESSAGES` yanıtında bulunur; diğer hatalarda alan hiç yer almaz. Reddedilen kayıtlar, `index` sırasıyla ilk 100 tanesi; 202 yanıtındaki `rejectedItems` ile aynıdır.
- `rejectedItems[].index` (integer, zorunlu): Kaydın istekteki `messages` dizisindeki sırası, sıfırdan başlar.
- `rejectedItems[].recipient` (string, null olabilir, zorunlu): Kayıttaki alıcı. Kayıt `null` ise ya da alıcısı yoksa `null`.
- `rejectedItems[].reason` (string, zorunlu): Değerler: `INVALID_RECIPIENT`, `INVALID_MESSAGE`, `MESSAGE_TOO_LONG`, `MALFORMED_ENTRY`, `DUPLICATE_RECIPIENT`, `NO_INTERNATIONAL_PERMISSION`.

  | Değer | Ne zaman |
  |---|---|
  | `INVALID_RECIPIENT` | Numara eksik ya da biçimi yanlış; bkz. `recipient`. |
  | `INVALID_MESSAGE` | Metin eksik ya da boş. |
  | `MESSAGE_TOO_LONG` | Metin, `encoding` ve gönderici türüne göre sınırı aşıyor; bkz. `encoding`. |
  | `MALFORMED_ENTRY` | Kayıt `null`. |
  | `DUPLICATE_RECIPIENT` | Aynı alıcı istekte daha sonra yeniden geçiyor. Yalnızca son geçen kayıt gönderilir, öncekiler bu nedenle reddedilir; son kayıt başka bir nedenle reddedilse bile. Numaralar yazıldığı gibi karşılaştırılır. Mükerrer gönderime izin verilen hesaplarda uygulanmaz. |
  | `NO_INTERNATIONAL_PERMISSION` | Numara yurt dışı (`-` ile başlıyor) ve hesabın yurt dışı gönderim izni yok. İzin için destek ekibiyle iletişime geçmelisiniz. Yalnızca başka bir hatası olmayan kayıtlarda bildirilir. |

  Bir kayıt birden fazla nedenle hatalıysa tek neden bildirilir; öncelik: `DUPLICATE_RECIPIENT`,
  `MALFORMED_ENTRY`, `MESSAGE_TOO_LONG`, `INVALID_RECIPIENT`, `INVALID_MESSAGE`,
  `NO_INTERNATIONAL_PERMISSION`.

- `rejectedItemsTruncated` (boolean, isteğe bağlı): Yalnızca `rejectedItems` ile birlikte bulunur. 100'den fazla kayıt reddedildiğinde true.

Örnek (şemadan üretildi):

```json
{
  "code": "INVALID_REQUEST",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```

### 401 — UNAUTHENTICATED. Bearer token yok ya da token geçersiz veya süresi dolmuş; `message` `no bearer token` ya da `invalid or expired token`.

Gövde (`application/json`): alanlar 400 yanıtıyla aynı; farklı olanlar:

- `code` (string, zorunlu): Değerler: `UNAUTHENTICATED`.

Örnek (şemadan üretildi):

```json
{
  "code": "UNAUTHENTICATED",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```

### 403 — FORBIDDEN. Hesap bu gönderimi yapamaz; `message` şunlardan biridir:

| `message` | Anlamı |
|---|---|
| `account is not activated for this operation` | Hesapta SMS gönderimi etkin değil. |
| `sub user may not use the sms api` | Alt kullanıcının SMS API'sini kullanma izni yok. |
| `account operator is not served for this operation` | Hesap bu OTP hizmetine tanımlı değil: kurumsal OTP hesabı `x-client: enterprise` göndermeden ya da diğer hesaplar bu başlıkla istek yaptı. |

Gövde (`application/json`): alanlar 400 yanıtıyla aynı; farklı olanlar:

- `code` (string, zorunlu): Değerler: `FORBIDDEN`.

Örnek (şemadan üretildi):

```json
{
  "code": "FORBIDDEN",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```

### 422 — İstek geçerli ama gönderim yapılamaz. `code` ve `message`:

| `code` | `message` | Anlamı ve çözüm |
|---|---|---|
| `REJECTED_HEADER` | `NO_HEADERS_REGISTERED` | Hesapta kayıtlı başlık yok. |
| `REJECTED_HEADER` | `NOT_REGISTERED` | `sender` hesaba kayıtlı değil. Kayıtlı başlıkları `GET /senders` ile görebilirsiniz. |
| `REJECTED_HEADER` | `ROUTING_CODE_MISSING` | Alfanümerik başlık için hesapta yönlendirme kodu tanımlı değil. |
| `REJECTED_HEADER` | `SUBSCRIBER_NUMBER_RESTRICTED` | Hesapta abone numarasından gönderim kısıtlı. |
| `INSUFFICIENT_CREDIT` | `no credit, package or campaign to pay for the otp` | Hesabın OTP gönderimini karşılayacak kredisi, paketi ya da kampanyası yok. Hiçbir şey gönderilmedi; bakiye yükledikten sonra yeniden deneyebilirsiniz. |

Gövde (`application/json`): alanlar 400 yanıtıyla aynı; farklı olanlar:

- `code` (string, zorunlu): Değerler: `REJECTED_HEADER`, `INSUFFICIENT_CREDIT`.

Örnek (şemadan üretildi):

```json
{
  "code": "REJECTED_HEADER",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```

### 500 — INTERNAL_ERROR, `message` `internal error`. Beklenmeyen hata; bildirirken `requestId` değerini paylaşmanız gerekir.

Gövde (`application/json`): alanlar 400 yanıtıyla aynı; farklı olanlar:

- `code` (string, zorunlu): Değerler: `INTERNAL_ERROR`.

Örnek (şemadan üretildi):

```json
{
  "code": "INTERNAL_ERROR",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```

### 503 — SERVICE_UNAVAILABLE. Servis ya da OTP kanalı geçici olarak kullanılamıyor. Hiçbir şey gönderilmedi; yeniden denemek güvenlidir.

Gövde (`application/json`): alanlar 400 yanıtıyla aynı; farklı olanlar:

- `code` (string, zorunlu): Değerler: `SERVICE_UNAVAILABLE`.

Örnek (şemadan üretildi):

```json
{
  "code": "SERVICE_UNAVAILABLE",
  "message": "string",
  "requestId": "string",
  "timestamp": "2026-01-01T10:00:00+03:00",
  "violations": [
    {
      "field": "string",
      "message": "string"
    }
  ],
  "rejectedItems": [
    {
      "index": 0,
      "recipient": "string",
      "reason": "INVALID_RECIPIENT"
    }
  ],
  "rejectedItemsTruncated": true
}
```
