# who.gen.tr API v1 — Entegrasyon rehberi

Bu rehber, who.gen.tr üzerinden alan adı kayıt bilgisi (WHOIS/RDAP) sorgulayacak projeler içindir. Makine tarafından okunabilir sözleşme [openapi.json](openapi.json) dosyasındadır; çalışan sunucuda `GET /openapi.json` adresinden de alınır. Hazır örnekler: [Node.js](examples/node.mjs), [PHP](examples/php.php).

## İçindekiler

1. [Hızlı başlangıç](#hızlı-başlangıç)
2. [Temel bilgiler](#temel-bilgiler)
3. [Kimlik doğrulama ve yetkiler](#kimlik-doğrulama-ve-yetkiler)
4. [Limitler ve kota](#limitler-ve-kota)
5. [Uç noktalar](#uç-noktalar)
6. [Hatalar](#hatalar)
7. [Yeniden deneme önerisi](#yeniden-deneme-önerisi)
8. [Sürümleme ve uyumluluk](#sürümleme-ve-uyumluluk)
9. [Kontrol listesi](#kontrol-listesi)

## Hızlı başlangıç

1. who.gen.tr yöneticisinden projeniz için bir API anahtarı isteyin. Anahtar `wgt_` ile başlar ve **yalnızca bir kez** gösterilir.
2. Anahtarı sunucu tarafında bir ortam değişkeninde saklayın (ör. `WHOIS_API_KEY`).
3. İlk sorguyu yapın:

```bash
curl -s -H "Authorization: Bearer $WHOIS_API_KEY" "https://who.gen.tr/api/v1/lookup?domain=example.com"
```

## Temel bilgiler

| Konu | Değer |
| --- | --- |
| Adres | `https://who.gen.tr` |
| Temel yol | `/api/v1` |
| Biçim | JSON, UTF-8. Tüm uçlar `GET`'tir; istek gövdesi yoktur. |
| Zaman | Tüm zamanlar ISO 8601, UTC (`2026-10-30T12:00:00Z`). Gün ve ay sınırları UTC'ye göredir. |
| İstek kimliği | Her yanıtta `X-Request-Id` başlığı ve gövdede `requestId` bulunur. Destek talebinde bu değeri iletin. |
| Önbellek başlığı | Anahtarlı ve ham veri isteyen yanıtlar `Cache-Control: no-store` taşır; bunları ara katmanda önbelleğe almayın. |
| Tarayıcı | API sunucudan sunucuya kullanım içindir. CORS başlığı gönderilmez; anahtarınızı tarayıcı koduna koymayın. |

## Kimlik doğrulama ve yetkiler

Anahtar `Authorization` başlığında gönderilir:

```http
Authorization: Bearer wgt_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

- Anahtar biçimi `wgt_` + 43 karakterdir (`A-Z a-z 0-9 _ -`). Başka biçimde gönderilen değer doğrudan `401 INVALID_API_KEY` alır.
- İptal edilmiş, süresi dolmuş, projesi devre dışı bırakılmış veya silinmiş anahtar `401 INVALID_API_KEY` alır. **Hatalı anahtar anonim erişime düşmez**; sorun anahtar tarafındadır.
- Anahtar yenilendiğinde (rotate) eski anahtar **hemen** geçersiz olur. Yenilemeyi dağıtımınızla aynı anda planlayın.
- Bir projenin birden fazla anahtarı olabilir; kota anahtar başına değil **proje başınadır**.

### Yetkiler (scope)

| Yetki | Ne sağlar |
| --- | --- |
| `lookup:read` | Normal sorgu sonucu. Her anahtarda bulunur. |
| `lookup:raw` | Ek olarak kaynağın ham yanıtı (`includeRaw=true`). Yönetici onayıyla verilir. |

Ham yanıt, kaynağın yayımladığı iletişim bilgilerini içerebilir. Bu yetkiyi alan proje kendi erişim ve saklama politikasını uygulamakla yükümlüdür.

### Anonim erişim

`Authorization` başlığı hiç gönderilmezse istek anonim sayılır. Anonim erişim yalnızca `GET /api/v1/lookup` içindir ve istemci IP'si başına dakikada 10, günde 100, ayda 1.000 sorguyla sınırlıdır. Üretim entegrasyonları her zaman anahtar kullanmalıdır.

## Limitler ve kota

Üç ayrı sınır vardır:

| Sınır | Kapsam | Değer | Aşılırsa |
| --- | --- | --- | --- |
| İstek hızı | İstemci IP'si; `lookup`, `usage` ve `capabilities` toplamı | Dakikada 120 istek | `429 RATE_LIMITED`, `Retry-After: 60` |
| Proje kotası | Proje (tüm anahtarlar ortak) | Günlük ve aylık; proje bazında yönetici belirler | `429 QUOTA_EXCEEDED`, `Retry-After: 3600` |
| Anonim kota | İstemci IP'si | Dakikada 10, günde 100, ayda 1.000 | `429 QUOTA_EXCEEDED`, `Retry-After: 60` veya `3600` |

Bilinmesi gerekenler:

- İstek hızı sınırı kimlik doğrulamadan önce, IP başına uygulanır. Aynı sunucudan çıkan tüm istekleriniz dakikada 120'yi paylaşır; kotanız daha yüksek olsa bile toplu işleri buna göre yayın.
- Kotadan yalnızca **başarılı** sorgular düşer. Hata dönen sorgular kotayı tüketmez. Önbellekten dönen yanıt da başarılı sorgudur ve kotadan düşer.
- Devam eden sorgular `reserved` olarak sayılır ve kalan hakkı geçici olarak azaltır.
- Günlük kota her gün 00:00 UTC'de, aylık kota her ayın 1'inde 00:00 UTC'de sıfırlanır. Güncel durum için [`GET /api/v1/usage`](#get-apiv1usage) kullanın.

## Uç noktalar

### `GET /api/v1/lookup`

Tek bir kayıtlı alan adının (registrable domain) kayıt bilgisini döner.

**Yetki:** `lookup:read` (anahtar veya anonim). `includeRaw=true` için ayrıca `lookup:raw`.
**Kota:** Başarılı sorgu proje kotasından 1 düşer.

| Parametre | Zorunlu | Açıklama |
| --- | --- | --- |
| `domain` | Evet | Kayıt edilebilir alan adı. Ayrıntılar aşağıda. |
| `includeRaw` | Hayır | `true` veya `false` (varsayılan `false`). Başka değer `400 INVALID_REQUEST` alır. |

**`domain` kuralları**

- Yalnızca kayıt edilebilir alan adı kabul edilir: `example.com`, `example.com.tr`, `example.co.uk`.
- Alt alan adı (`www.example.com`, `mail.example.com.tr`), URL (`https://example.com/`), IP adresi, port veya yol içeren değerler `400 INVALID_DOMAIN` alır. API bunları sessizce dönüştürmez; ayıklamayı istemci yapmalıdır.
- Uluslararası alan adları (IDN) kabul edilir: `çiçek.com.tr` ve `xn--iek-1lab.com.tr` aynı sorgudur. Büyük harf küçültülür, baştaki ve sondaki boşluk ile sondaki nokta yok sayılır.
- En fazla 253 bayt; her etiket en fazla 63 bayt.
- URL parametresi olduğu için değeri her zaman kodlayın (`encodeURIComponent`, `rawurlencode`).

**Kaynak seçimi:** `.tr` alan adları TRABIS WHOIS'ten, diğerleri IANA RDAP kaydındaki RDAP sunucusundan, RDAP yoksa ilgili WHOIS sunucusundan sorgulanır. İstemci kaynak seçemez.

**Süre:** Sunucu bir sorgu için en fazla yaklaşık 12 saniye harcar. İstemci zaman aşımını **en az 15 saniye** tutun.

#### Başarılı yanıt — `200 OK`

```json
{
  "requestId": "6f1c2b8e-3a1d-4f0e-9b7a-2d5c8e1f4a90",
  "result": {
    "domain": { "ascii": "example.com", "unicode": "example.com" },
    "registration": "registered",
    "registrar": "RESERVED-Internet Assigned Numbers Authority",
    "dates": {
      "created": { "value": "1995-08-14T04:00:00Z", "precision": "second" },
      "updated": { "value": "2026-08-14T07:01:39Z", "precision": "second" },
      "expires": { "value": "2027-08-13T04:00:00Z", "precision": "second" }
    },
    "nameservers": ["a.iana-servers.net", "b.iana-servers.net"],
    "statuses": ["client delete prohibited", "client transfer prohibited", "client update prohibited"],
    "dnssec": true,
    "source": { "protocol": "rdap", "authority": "rdap.verisign.com", "checkedAt": "2026-10-30T09:15:02Z" },
    "fetchedAt": "2026-10-30T09:15:02Z",
    "warnings": []
  },
  "cache": { "hit": false, "expiresAt": "2026-10-30T09:45:02Z" }
}
```

Değerler örnektir.

**`result` alanları**

| Alan | Tür | Açıklama |
| --- | --- | --- |
| `domain.ascii` / `domain.unicode` | string | Alan adının ASCII (punycode) ve Unicode yazımı. |
| `registration` | `registered` \| `not_found` \| `unknown` | Kayıt durumu. Aşağıya bakın. |
| `registrar` | string \| null | Kayıt kuruluşu; kaynak vermezse `null`. |
| `dates.created` / `updated` / `expires` | `DateValue` \| null | Kayıt, güncelleme ve bitiş tarihi; kaynak vermezse `null`. |
| `nameservers` | string[] | Ad sunucuları, küçük harf. Boş olabilir. |
| `statuses` | string[] | Kaynağın bildirdiği durum kodları (ör. EPP durumları), olduğu gibi. |
| `dnssec` | boolean \| null | DNSSEC imzalı mı; bilinmiyorsa `null`. |
| `source.protocol` | `rdap` \| `whois` \| `trabis` | Verinin alındığı protokol. |
| `source.authority` | string | Verinin alındığı sunucu. |
| `source.checkedAt` | tarih-saat | Kaynağın sorgulandığı an. |
| `fetchedAt` | tarih-saat | Sonucun üretildiği an. Önbellekten gelen yanıtta ilk sorgunun zamanıdır. |
| `warnings` | string[] | Sonucun eksik olduğu noktalar. |

**`DateValue`:** `{ "value": "...", "precision": "date" | "second" }`. Kaynak yalnızca tarih veriyorsa `value` `YYYY-MM-DD` biçimindedir ve `precision` `date` olur; uydurma saat eklenmez. Saat varsa `value` UTC tarih-saattir ve `precision` `second` olur. Gün hesabı yaparken iki biçimi de destekleyin.

**`registration` değerleri**

| Değer | Anlamı | Ne yapmalı |
| --- | --- | --- |
| `registered` | Alan adı kayıtlı. | Alanları kullanın. |
| `not_found` | Kaynak, alan adının kayıtlı olmadığını açıkça bildirdi. | Alan adı boşta kabul edilebilir. Yine de satın alma öncesi kayıt kuruluşunda doğrulayın. |
| `unknown` | Kaynak yanıt verdi ama durum güvenilir biçimde belirlenemedi. | "Boşta" olarak **yorumlamayın**. |

Kaynak hatası hiçbir zaman `not_found` olarak dönmez; hata yanıtı (5xx) döner.

**`warnings` değerleri:** Şu an `registrar_unavailable` (kaynak kayıt kuruluşu bilgisi vermedi) üretilir. Liste ileride genişleyebilir; tanımadığınız kodları yok sayın.

**`cache` alanı**

| Alan | Açıklama |
| --- | --- |
| `hit` | `true` ise sonuç ortak önbellekten geldi. |
| `expiresAt` | Önbellek kaydının bitiş zamanı; önbelleğe alınmadıysa `null`. |

`registered` sonuçlar varsayılan olarak 30 dakika (süre servis tarafından değiştirilebilir; kesin bitiş `cache.expiresAt` alanındadır), `not_found` sonuçlar en fazla 2 dakika önbellekte tutulur; `unknown` önbelleğe alınmaz. Aynı alan adı için kısa aralıklarla tekrar sorgu yapmak taze veri getirmez, yalnızca kota tüketir. Kendi tarafınızda da `cache.expiresAt` süresince sonucu saklayabilirsiniz.

#### Ham yanıt — `includeRaw=true`

`lookup:raw` yetkili anahtarla yanıta `raw` alanı eklenir:

```json
{
  "requestId": "…",
  "result": { "…": "…" },
  "cache": { "hit": false, "expiresAt": null },
  "raw": { "contentType": "application/rdap+json", "text": "{\"objectClassName\":\"domain\", …}" }
}
```

- `contentType` RDAP için `application/rdap+json`, WHOIS/TRABIS için `text/plain`'dir. `text` her zaman string'dir; RDAP'te JSON'u ayrıca ayrıştırmanız gerekir.
- Ham istek önbellekten yanıtlanmaz ve önbelleğe yazılmaz; bu yüzden her çağrı kaynağa gider. Yalnızca gerektiğinde kullanın.
- Yetkisiz anahtar veya anonim istek `403 INSUFFICIENT_SCOPE` alır.

### `GET /api/v1/usage`

Projenin güncel kota durumunu döner. Anahtar zorunludur; anahtarsız istek `401 INVALID_API_KEY` alır. Kotadan düşmez.

```bash
curl -s -H "Authorization: Bearer $WHOIS_API_KEY" "https://who.gen.tr/api/v1/usage"
```

```json
{
  "requestId": "…",
  "usage": {
    "day": {
      "limit": 5000, "successful": 1240, "reserved": 2, "remaining": 3758,
      "periodStart": "2026-10-30T00:00:00.000Z", "periodEnd": "2026-10-31T00:00:00.000Z"
    },
    "month": {
      "limit": 100000, "successful": 38211, "reserved": 2, "remaining": 61787,
      "periodStart": "2026-10-01T00:00:00.000Z", "periodEnd": "2026-11-01T00:00:00.000Z"
    }
  }
}
```

`remaining = limit − successful − reserved` (en az 0). `periodEnd` kotanın sıfırlanacağı andır.

### `GET /api/v1/capabilities`

Bir uzantı (TLD) için hangi kaynağın kullanılacağını ve sistemin o kaynakla daha önce başarılı sorgu yapıp yapmadığını döner. Anahtar gerekmez, kotadan düşmez.

| Parametre | Zorunlu | Açıklama |
| --- | --- | --- |
| `tld` | Evet | Noktasız uzantı: `tr`, `com`, `com.tr`. En fazla 100 karakter. |

```bash
curl -s "https://who.gen.tr/api/v1/capabilities?tld=tr"
```

```json
{
  "requestId": "…",
  "tld": "tr",
  "known": true,
  "source": { "protocol": "trabis", "authority": "whois.trabis.gov.tr" },
  "status": "supported",
  "testedAt": "2026-10-30T08:59:41.000Z"
}
```

| Alan | Açıklama |
| --- | --- |
| `known` | Uzantı için genel bir kayıt kaynağı biliniyorsa `true`. `false` ise bu uzantıda sorgu `422 UNSUPPORTED_TLD` alır. |
| `source` | Kullanılacak protokol ve sunucu; bilinmiyorsa `null`. |
| `status` | `supported`: etkin bir sorgu düğümü bu kaynakla başarılı sorgu yaptı. `unknown`: henüz gözlenmedi; sorgu yine de çalışabilir. |
| `testedAt` | Son başarılı gözlem; `unknown` ise `null`. |

Kaynak seçimi: `.tr` için TRABİS; uzantı IANA RDAP listesindeyse oradaki RDAP sunucusu; listede olmayan ama kendi RDAP hizmeti olan kayıt kuruluşları için o hizmet (`.de`, `.ch`, `.li`, `.us`, `.co`, `.io`, `.me`, `.ac`, `.sh`); hiçbiri yoksa IANA'nın bildirdiği WHOIS sunucusu. WHOIS yanıtlarının biçimi kuruluşa göre değişir. Kayıt kanıtı (kayıt firması, tarih, ad sunucusu veya durum) içermeyen bir yanıt hiçbir zaman `registered` sayılmaz; `502 UPSTREAM_ERROR` döner. Kuruluşların yayımlamadığı alanlar `null` kalır (ör. `.de` kayıt firması ve tarihleri, `.eu` tarihleri).

### Sağlık ve sözleşme

| Uç | Yanıt |
| --- | --- |
| `GET /health/live` | `200 {"status":"live","requestId":"…"}` — süreç ayakta. |
| `GET /health/ready` | `200 {"status":"ready",…}` veya `503 {"status":"unready",…}` — veritabanı hazır mı. |
| `GET /openapi.json` | OpenAPI 3.1 sözleşmesi. |

Sağlık uçları izleme içindir; her sorgudan önce çağırmayın.

## Hatalar

Tüm hatalar aynı gövdeyi kullanır:

```json
{
  "error": { "code": "QUOTA_EXCEEDED", "message": "Lookup quota exceeded", "retryAfterSeconds": 3600 },
  "requestId": "…"
}
```

- Mantığınızı `error.code` üzerine kurun. `message` insan içindir, değişebilir; 5xx hatalarında her zaman `Service unavailable` döner.
- `retryAfterSeconds` doluysa aynı değer `Retry-After` başlığında da (saniye) gelir.

| HTTP | `code` | Anlamı | Ne yapmalı |
| --- | --- | --- | --- |
| 400 | `INVALID_DOMAIN` | `domain` eksik veya kayıt edilebilir alan adı değil. | Girdiyi düzeltin; tekrar denemeyin. |
| 400 | `INVALID_REQUEST` | Geçersiz parametre (`includeRaw`, `tld`). | İsteği düzeltin. |
| 401 | `INVALID_API_KEY` | Anahtar hatalı, iptal edilmiş, süresi dolmuş veya proje kapalı; `usage` için anahtar yok. | Anahtarı kontrol edin, yöneticiyle görüşün. |
| 403 | `INSUFFICIENT_SCOPE` | Gerekli yetki yok (çoğunlukla `lookup:raw`). | `includeRaw` kullanmayın veya yetki isteyin. |
| 404 | `NOT_FOUND` | Böyle bir uç yok. | Yolu kontrol edin. |
| 422 | `UNSUPPORTED_TLD` | Uzantı için genel kayıt kaynağı yok. | Tekrar denemeyin; kullanıcıya desteklenmediğini gösterin. |
| 429 | `RATE_LIMITED` | IP başına dakikalık istek sınırı aşıldı. | `Retry-After` kadar bekleyin, hızınızı düşürün. |
| 429 | `QUOTA_EXCEEDED` | Günlük/aylık (anonimde dakikalık) kota doldu. | `GET /usage` ile `periodEnd`'i kontrol edin; o zamana kadar durun. |
| 500 | `INTERNAL_ERROR` | Beklenmeyen sunucu hatası. | Bir kez geri çekilerek deneyin; sürerse `requestId` ile bildirin. |
| 502 | `UPSTREAM_ERROR` | Kayıt kaynağı hata verdi veya yanıtı anlaşılamadı. | Geri çekilerek deneyin. |
| 503 | `UPSTREAM_RATE_LIMITED` | Kayıt kaynağı bizi geçici olarak sınırladı. | `Retry-After` kadar (30 sn – 1 saat) bekleyin. |
| 503 | `NO_CAPACITY` | Tüm sorgu düğümleri meşgul. | `Retry-After` (birkaç saniye) sonra deneyin. |
| 504 | `LOOKUP_TIMEOUT` | Sorgu süre sınırını aştı. | Kısa bekleyip bir kez daha deneyin. |

Hata yanıtları kotadan düşmez.

## Yeniden deneme önerisi

- **4xx (429 hariç):** Tekrar denemeyin; istek veya anahtar düzeltilmelidir.
- **429 `RATE_LIMITED`:** `Retry-After` kadar bekleyin.
- **429 `QUOTA_EXCEEDED`:** Dönem sonuna kadar yeni sorgu göndermeyin.
- **5xx:** En fazla 2–3 deneme. `Retry-After` varsa ona uyun; yoksa üstel geri çekilme (ör. 1 sn, 2 sn, 4 sn) ve rastgele sapma ekleyin.
- Toplu işlerde eşzamanlılığı düşük tutun (ör. 2–4 paralel istek) ve aynı alan adını önbellek süresi dolmadan tekrar sorgulamayın.

Node.js örneği (Node 18+):

```js
const sleep = ms => new Promise(r => setTimeout(r, ms));

export async function lookup(domain, { attempts = 3 } = {}) {
  for (let i = 1; ; i++) {
    const res = await fetch(`https://who.gen.tr/api/v1/lookup?domain=${encodeURIComponent(domain)}`, {
      headers: { Authorization: `Bearer ${process.env.WHOIS_API_KEY}`, Accept: 'application/json' },
      signal: AbortSignal.timeout(15000),
    });
    const body = await res.json();
    if (res.ok) return body;
    const retryable = res.status >= 500 || body.error?.code === 'RATE_LIMITED';
    if (!retryable || i >= attempts) throw Object.assign(new Error(body.error?.code ?? `HTTP ${res.status}`), { status: res.status, body });
    const wait = Number(res.headers.get('retry-after')) || 2 ** (i - 1);
    await sleep(wait * 1000 + Math.random() * 250);
  }
}
```

## Sürümleme ve uyumluluk

- Sürüm yolda taşınır: `/api/v1`. Alan silme, alan anlamını veya türünü değiştirme gibi kırıcı değişiklikler yalnızca yeni ana sürümde (`/api/v2`) yapılır.
- v1 içinde yanıtlara **yeni alanlar**, `warnings` listesine yeni kodlar ve yeni hata kodları eklenebilir. İstemciniz tanımadığı alanları ve kodları yok saymalıdır; JSON'u katı şemayla reddetmeyin.

## Kontrol listesi

- [ ] Anahtar yalnız sunucu ortam değişkeninde; kaynak koda, loglara ve tarayıcıya girmiyor.
- [ ] `domain` istemci tarafında kayıt edilebilir alan adına indirgeniyor ve URL kodlanıyor.
- [ ] İstemci zaman aşımı en az 15 saniye.
- [ ] Mantık `error.code` üzerine kurulu; `Retry-After`'a uyuluyor.
- [ ] `unknown` sonucu "boşta" sayılmıyor.
- [ ] Sonuçlar `cache.expiresAt` süresince yerelde tutuluyor; aynı alan adı gereksiz yere sorgulanmıyor.
- [ ] Hata loglarına `requestId` yazılıyor.
- [ ] Kota durumu `GET /api/v1/usage` ile izleniyor.
