# İletim raporu webhook'u kaydetme

> Hesabın iletim raporlarının gönderileceği adres.

- **İstek:** `POST https://ext-api.netgsm.com.tr/sms/v3/webhooks/report`
- **Canlı ortam:** `https://ext-api.netgsm.com.tr/sms/v3`
- **Kimlik doğrulama:** `Authorization: Bearer <token>`
- **HTML sayfası:** https://api-docs.netgsm.com.tr/register-report-webhook/
- **OpenAPI tanımı:** https://api-docs.netgsm.com.tr/openapi/sms-v3.yaml (operationId: `registerReportWebhook`)

## Açıklama

Hesabın iletim raporlarının gönderileceği adres. Hesap başına bir tane; var olanın
üzerine kayıt 409 ile reddedilir; önce mevcut kaydı silmeniz gerekir. Etkinleştirme gerekmez; alt kullanıcı için SMS
menüsü gerekir.

## İstek

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

- `url` (string, zorunlu, en fazla uzunluk 1024): Host'u yalnızca genel (public) adreslere çözümlenen mutlak bir http ya da https URL - loopback, özel, link-local ya da başka bir şekilde iç adres olamaz. Port verilirse 1 ile 65535 arasındadır.
- `headers` (string, isteğe bağlı, en fazla uzunluk 500): Webhook adresinizin Netgsm'den beklediği başlık; örneğin adresiniz bir kimlik doğrulama bilgisi istiyorsa "Authorization: Bearer <token>". Netgsm bu başlığı adresinize yaptığı her istekte aynen gönderir; böylece isteğin Netgsm'den geldiğini doğrulayabilirsiniz. Tek bir satır, `Name: value` biçiminde; yalnızca yazdırılabilir ASCII, satır sonu olmadan. Değer virgül (,) ya da iki nokta üst üste (:) içeremez. İsteğe bağlı; boş bırakılırsa ek başlık gönderilmez.

Örnek gövde:

```json
{
  "url": "https://example.com/report",
  "headers": "Authorization: Bearer token123"
}
```

## Örnek istek

```bash
curl -X POST 'https://ext-api.netgsm.com.tr/sms/v3/webhooks/report' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://example.com/report",
  "headers": "Authorization: Bearer token123"
}'
```

## Yanıtlar

### 201 — Kaydedildi.

Gövde (`application/json`):

- `url` (string, zorunlu)
- `headers` (string, null olabilir, zorunlu): Kayıtta verilen başlık satırı; verilmediyse `null`.
- `registeredAt` (string, date-time, zorunlu): Webhook'un kaydedildiği zaman.

Örnek (şemadan üretildi):

```json
{
  "url": "string",
  "headers": "string",
  "registeredAt": "2026-01-01T10:00:00+03:00"
}
```

### 400 — INVALID_REQUEST. `url` eksik, çok uzun ya da host içeren mutlak bir http(s) URL değil; host'u çözümlenmiyor ya da yalnızca genel (public) adreslere çözümlenmiyor; `headers` çok uzun, tek bir `Name: value` satırı değil ya da değerinde virgül veya iki nokta üst üste var; gövde JSON değil.

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. SMS menüsü olmayan bir alt kullanıcı.

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
}
```

### 409 — ALREADY_EXISTS. Kayıtlı bir iletim raporu webhook'u var; önce silmeniz gerekir. Hesap için başka bir iletim raporu webhook kaydı sürerken de döner; o tamamlandığında istek yeniden gönderilebilir.

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

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

Örnek (şemadan üretildi):

```json
{
  "code": "ALREADY_EXISTS",
  "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
}
```

### 429 — RATE_LIMITED. Hesap için bu uç noktanın sınırı aşıldı; `message` sınırı belirtir. `Retry-After` yalnızca hesabın gönderimi askıya alındığında bulunmaz.

Başlıklar:

- `Retry-After` (integer): İsteğin tekrarlanabileceği zamana kadar kalan tam saniye.

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

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

Örnek (şemadan üretildi):

```json
{
  "code": "RATE_LIMITED",
  "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. Hiçbir şey yapılmadı; 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
}
```

## Geri çağrılar (webhook)

### deliveryReport: `POST {$request.body#/url}`

İletim raporları webhook adresinize gönderilir

İletim raporları, kayıtlı adrese periyodik olarak ve toplu hâlde `POST` ile gönderilir.
Bir istek, hesabın bekleyen raporlarından bir grubu `numbers` dizisinde taşır. Bekleyen rapor
sayısı fazlaysa raporlar aynı periyotta birden fazla isteğe bölünebilir.

- Kayıt sırasında `headers` verildiyse o başlık her istekte gönderilir; isteğin
  Netgsm'den geldiğini bu başlıkla doğrulayabilirsiniz.
- Bağlantının 60 saniye içinde kurulması, yanıtın 10 dakika içinde gelmesi gerekir; aksi
  halde istek başarısız sayılır.
- Herhangi bir `2xx` yanıt, raporların teslim alındığı anlamına gelir. Yanıt gövdesi
  okunmaz.
- Zaman aşımı, `2xx` dışındaki bir yanıt ya da bağlantı hatası durumunda o istekteki
  raporlar **yeniden gönderilmez**. Bu raporlara `GET /report` ile ulaşılabilir.
- Zamanlar saat dilimi bilgisi olmadan, Türkiye saatiyle (UTC+3) `yyyy-MM-dd HH:mm:ss`
  biçimindedir.
- `status`, `operator` ve `errorCode` sayısal kodlardır; `GET /report` ise aynı bilgiyi
  ad olarak döndürür. Karşılıkları alan açıklamalarındaki tablolardadır.

Gönderilen gövde (`application/json`, zorunlu)

- `numbers` (array<object>, zorunlu): Bu istekle gönderilen raporlar; her alıcı için bir satır.
- `numbers[].jobid` (string, isteğe bağlı): Send uç noktasının döndürdüğü görev numarası (`jobId`).
- `numbers[].number` (string, isteğe bağlı): Alıcı numarası.
- `numbers[].status` (integer, isteğe bağlı): İletim sonucu, sayısal kod. Bekleyen (henüz sonuçlanmamış) gönderimler webhook ile bildirilmez; yalnızca sonuçlananlar gelir.

  | Kod | Anlamı | `GET /report` karşılığı |
  |---|---|---|
  | `1` | İletildi | `DELIVERED` |
  | `2` | Ulaştırılamadı; nedeni `errorCode` alanındadır | `NOT_DELIVERED` |
  | `3` | Hatalı numara | `NOT_DELIVERED`, `INVALID_NUMBER` |
  | `4` | Operatöre gönderilemedi | `NOT_DELIVERED`, `NOT_SENT_TO_OPERATOR` |
  | `8` | Operatör kabul etmedi | `NOT_DELIVERED`, `OPERATOR_REJECTED` |
  | `11` | Operatör hatası | `NOT_DELIVERED`, `OPERATOR_ERROR` |
  | `12` | Gönderim hatası | `NOT_DELIVERED`, `SEND_ERROR` |
  | `13` | Mükerrer | `NOT_DELIVERED`, `DUPLICATE` |
  | `14` | Kredisi yetmedi | `NOT_DELIVERED`, `INSUFFICIENT_CREDIT` |
  | `15` | Kara liste | `NOT_DELIVERED`, `BLACKLISTED` |
  | `16` | İYS ret | `NOT_DELIVERED`, `IYS_REJECTED` |
  | `17` | İYS hata | `NOT_DELIVERED`, `IYS_ERROR` |
  | `18` | Mesaj oluşturulamadı | `NOT_DELIVERED`, `MESSAGE_NOT_CREATED` |
  | `19` | Hatalı numara formatı | `NOT_DELIVERED`, `INVALID_NUMBER_FORMAT` |
  | `20` | NTS'den onay alınamadı | `NOT_DELIVERED`, `NTS_NOT_APPROVED` |
  | `22` | Yurt dışı gönderim izni yok | `NOT_DELIVERED`, `NO_INTERNATIONAL_PERMISSION` |

- `numbers[].operator` (integer, isteğe bağlı): Alıcının operatörü, sayısal kod.

  | Kod | Operatör | `GET /report` karşılığı |
  |---|---|---|
  | `10` | Vodafone | `VODAFONE` |
  | `20` | Türk Telekom | `TURK_TELEKOM` |
  | `30` | Turkcell | `TURKCELL` |
  | `40`, `41`, `43` | Netgsm | `NETGSM` |
  | `45` | Akontel | `AKONTEL` |
  | `210`–`218` | Yurt dışı | `INTERNATIONAL` |

  Operatörü belirlenemeyen kayıtlarda (örneğin numara hatalıysa) `0` gelir.

- `numbers[].msglen` (integer, isteğe bağlı): Mesajın kapladığı SMS parça sayısı.
- `numbers[].deliveredDate` (string, isteğe bağlı): İletildiği zaman, `yyyy-MM-dd HH:mm:ss` biçiminde, Türkiye saatiyle. Tarih bilgisi yoksa boş string (`""`).
- `numbers[].errorCode` (integer, null olabilir, isteğe bağlı): İletilmeme nedeni. **Yalnızca `status` `2` olduğunda anlamlıdır**; diğer durumlarda nedeni `status` belirtir ve bu alanın değeri dikkate alınmamalıdır.

  | Kod | Anlamı | `GET /report` `failReason` |
  |---|---|---|
  | `111` | Zaman aşımı | `TIMEOUT` |
  | `112` | Mobil cihaz SMS gönderimine kapalı | `SMS_DISABLED_ON_DEVICE` |
  | `113` | Mobil cihaz desteklemiyor | `DEVICE_NOT_SUPPORTED` |
  | `114` | Yönlendirme başarısız | `ROUTING_FAILED` |
  | `115` | Hedef operatörden zaman aşımı | `OPERATOR_TIMEOUT` |
  | `116` | Tanımlanamayan abone | `UNKNOWN_SUBSCRIBER` |
  | `117` | Yasadışı abone | `ILLEGAL_SUBSCRIBER` |
  | `119` | Sistemsel hata | `SYSTEM_ERROR` |

  `status` `2` iken `0` ya da `null` gelirse neden bilinmiyordur (`GET /report`'ta `UNKNOWN`).

- `numbers[].referansID` (string, null olabilir, isteğe bağlı): Gönderimde verilen referans (`GET /report` yanıtındaki `referenceId`). Verilmediyse `null`.

Örnek gövde:

```json
{
  "numbers": [
    {
      "jobid": "179309912974612345062827",
      "number": "5xxxxxxxxx",
      "status": 1,
      "operator": 10,
      "msglen": 1,
      "deliveredDate": "2026-10-27 14:06:12",
      "errorCode": 0,
      "referansID": "8f3a1c"
    },
    {
      "jobid": "179309912974612345062827",
      "number": "5xxxxxxxxx",
      "status": 2,
      "operator": 30,
      "msglen": 1,
      "deliveredDate": "2026-10-27 14:09:40",
      "errorCode": 115,
      "referansID": null
    }
  ]
}
```

Beklenen yanıtlar:

- 200: Raporlar teslim alındı. Herhangi bir `2xx` yanıt aynı anlama gelir.
