<!-- Uebersetzung von HANDBUCH.md (tr), Stand 2026-10-09. Massgeblich ist die deutsche Fassung; je Ueberschrift eine Abschnitts-Marke (docs/api/apidoku.go). -->
# TourAPI – API El Kitabı v1
<!-- de:7006b6403a99 -->

Bir seyahat portalını veya bir tur operatörü sistemini TourAPI'ye bağlayan geliştiriciler için.
Durum: 30.09.2026, API sürümü `v1`; değişiklikler değişiklik günlüğünde yer alır. Makine tarafından okunabilir: [`openapi.yaml`](openapi.yaml)
(OpenAPI 3.1). Yapay zeka ajanları (Claude Code, Codex, Antigravity) için: akış, biçimler ve
yeniden deneme kurallarının özeti portalda `/doku/agenten.md` altında; giriş yaptıktan sonra kendi
erişiminizle doldurulmuş olarak (“Yapay zeka ajanları” sayfası, `AGENTS.md` olarak indirme).

**Bu el kitabındaki her örnek bir testtir.** İstekler
[`beispiele/`](beispiele/) altında (JetBrains ve VS Code REST istemcilerinin formatında) yer alır ve
her derlemede test verileriyle yeni başlatılmış bir TourAPI örneğine karşı çalıştırılır; gösterilen yanıtlar
bu sırada gerçek yanıtlarla karşılaştırılır.

| Kısaltma | Anlamı | Endpoint |
|---|---|---|
| BA | Müsaitlik ve fiyat sorgulama, arama | `/v1/search`, `/v1/price`, `/v1/prices` |
| BA | sabit tarih olmadan açık arama (anahtar başına yetki) | `/v1/search/open` (Bölüm 4a) |
| BA | bir otelin tarih matrisi (anahtar başına yetki) | `/v1/search/open/dates` (Bölüm 4b) |
| – | kendi anahtarının geçerli sınırları | `/v1/limits` (Bölüm 4c) |
| B | rezervasyon yapma | `/v1/book` |
| – | Rezervasyon bilgisini okuma | `/v1/booking` |
| S | iptal etme | `/v1/cancel` |
| – | Düğümün sağlık durumu | `/v1/health` (Bölüm 1.8) |
| – | EDF teslimatı (önbellek dışa aktarımı) | `/v1/export/edf/full`, `/changes`, `/ack` (Bölüm 10) |
| – | Sandbox: test anahtarları, test rezervasyonları, senaryolar | test anahtarıyla tüm endpoint'ler (Bölüm 11) |
| – | Otel içerikleri (ana veriler, konum, metinler, görseller, olanaklar) | `/v1/content/hotels`, `/changes`, `/catalog` (Bölüm 12) |

Konsol/ajanlar: Alıcı API'sinin parçası değildir (tur operatörü konsolunun `POST /changes` endpoint'i,
oturum üzerinden giriş, API anahtarı yok; açıklama ve sınırlar işletmeciden talep üzerine).

---

## 1. Başlangıç
<!-- de:151881b9cd96 -->

### 1.1 Erişim
<!-- de:883f83c4f74c -->

- Her istek API anahtarını **`X-Api-Key`** header'ında taşır. Oturum yoktur, token yoktur.
- Anahtarı tur operatörü verir (erişim paketi). Bir anahtar tam olarak **bir
  tur operatörüne** (kiracı) ve isteğe bağlı olarak bir **müşteri grubuna** aittir. Müşteri grubu
  özel fiyatları ve, kontenjan grubu olarak, rezervasyonun yapıldığı kontenjanı belirler. Grubu
  olmayan bir anahtar temel sözleşme üzerinde çalışır. Bir müşteri grubunun hangi türde olduğunu
  tur operatörü belirler:
  - **Kontenjan grubu** (kontenjanlı satış ortağı): yalnızca kendisine
    tahsis yapılmış otelleri görür ve bu tahsise karşı rezervasyon yapar. Diğer oteller bu anahtar için
    yoktur (`404 ERR_HOTEL_NOT_FOUND`, aramada ve `/v1/destinations` içinde yer almaz). O anda
    hiç tahsisi yoksa hiçbir otel görmez.
  - **Fiyat grubu**: tur operatörünün tüm otellerini görür ve genel
    stoktan grubu olmayan bir anahtar gibi rezervasyon yapar – yalnızca kendi fiyatıyla.
- Bir müşteri grubunun özel fiyatı (tahsisli veya tahsissiz) bir otel için
  hesaplanamıyorsa (tur operatöründe hatalı fiyat kampanyası), otel bu grup için
  yoktur (`404 ERR_HOTEL_NOT_FOUND`, aramada `diagnostics.reasons` içinde neden `ERR_HOTEL_NOT_FOUND`,
  `/v1/book` de `404` ile reddeder) – asla sessizce temel fiyat kullanılmaz.
- Kiracı, müşteri grubu ve fiyatlar **yalnızca anahtardan** gelir, asla bir parametreden gelmez.
  Diğer tur operatörlerinin otelleri bu anahtar için yoktur (`404`).
- Eksik, bilinmeyen veya iptal edilmiş anahtar: `401 ERR_UNAUTHORIZED`. Askıya alınmış bir
  tur operatörünün geçerli anahtarı: `403 ERR_TENANT_SUSPENDED`. Devre dışı bırakılmış bir
  müşteri grubunun anahtarı: `403 ERR_KEY_GROUP_INACTIVE` (asla sessizce temel sözleşme).
- Anahtarlar tarayıcı koduna veya loglara konmaz. Anahtarını kaybeden, anahtarı
  tur operatörüne iptal ettirir – ya da (tur operatörü etkinleştirdiyse) iş ortağı portalında
  kendisi iptal eder.
- **İş ortağı portalındaki anahtarlar:** Orada erişiminizin anahtarlarını görür, tur operatörünün
  serbest bıraktığı canlı anahtarları teslim alır, canlı anahtarları yeniler ve kendi
  anahtarlarınızı iptal edersiniz. Portal ham değeri yalnızca bir kez gösterir. API iptal edilen
  bir anahtarı birkaç saniye içinde reddeder (`401`).
- **Yenileme:** Yeni canlı anahtar aynı haklara sahiptir (müşteri grubu, dışa aktarma, içerik
  yetkisi, arama profili). Önceki anahtar tur operatörünün belirlediği süre boyunca geçerli kalır (varsayılan
  24 saat, 1 saat ile 7 gün arası) ve ardından saniyesinde reddedilir (`401 ERR_UNAUTHORIZED`).
  Bu süre içinde tüm sistemleri yeni anahtara geçirin. Yeni anahtarı ancak önceki anahtarın
  süresi dolduktan sonra yeniden yenileyebilirsiniz – böylece aynı anda en fazla iki anahtar
  geçerli olur.
- Geliştirme için **test anahtarları** (`tk_test_…`) vardır: canlı anahtarla aynı veriler, rezervasyonlar
  yalnızca sandbox'ta (Bölüm 11). Her yanıt modu `X-TourAPI-Mode` header'ında belirtir.

### 1.2 Temel URL, iletim, sürüm
<!-- de:dcd2ff977925 -->

- Temel URL erişim paketinde yer alır; örneklerde `{{baseUrl}}`.
- Tüm yollar **`/v1`** ile başlar. `v1` içinde nelerin değişebileceğini
  uyumluluk taahhüdü düzenler (Bölüm 1.9). Bilinmeyen yanıt alanlarını lütfen yok sayın.
- İstekler: UTF-8 JSON, `Content-Type: application/json`, en fazla **1 MiB** body.
  `GET` olanlar `/v1/booking`, `/v1/destinations`, `/v1/health` ve EDF teslimatıdır
  (`/v1/export/edf/full`, `/changes`), diğerlerinin tümü `POST`'tur; yanlış metot:
  `405 ERR_METHOD_NOT_ALLOWED`.
- Yanıtlardaki zaman damgaları (`bookedAt`, `updatedAt`) UTC olarak RFC 3339 formatındadır, ör.
  `2026-09-26T08:15:03Z`.
- Sunucu bir bağlantıyı 15 sn okuma veya 15 sn yazma sonrasında keser. İstisna:
  EDF teslimatının paketleri (`/v1/export/edf/full`, `/changes`) 10 dakikaya kadar yazabilir (Bölüm 10).
- Bilinmeyen bir yol (ör. `/v1/…` içinde yazım hatası) JSON gövdesi **olmadan** `404` döner
  (`text/plain`, `errorCode` yok); diğer tüm hatalar 1.6'daki biçimdedir.

### 1.3 Tarih, konaklama, referans tarihi
<!-- de:29109e02a38c -->

- Tarih alanları saat ve saat dilimi olmadan **`JJJJ-MM-TT`** biçiminde takvim günleridir.
- Bir konaklama yarı açıktır: `checkIn` ilk gecedir, `checkOut` ayrılış günüdür.
  `checkIn=2026-10-26`, `checkOut=2026-10-28` **iki gecedir**.
- **Referans tarihi**, Europe/Berlin saat dilimindeki sunucu tarihidir. Erken rezervasyon ve
  dönem kuralları her zaman bununla hesaplanır. İstek alanı `now` artık yalnızca tolere edilir: boş
  bırakın. Farklı bir tarih gönderilirse, sorgu yine de sunucu tarihiyle hesaplanır
  ve bunu `warnings` içinde belirtir; `/v1/book` farklı bir `priceCheck.now` değerini reddeder
  (`ERR_NOW_MISMATCH`).
- İstek başına sınırlar (her hesaplamadan önce devreye girer):

| Sınır | Değer | Kod (422) |
|---|---|---|
| Konaklama başına gece | 1–30 | `ERR_EMPTY_STAY`, `ERR_STAY_TOO_LONG` |
| En erken giriş | bugün (referans tarihi) | `ERR_STAY_IN_PAST` |
| En geç giriş | referans tarihi + 732 gün | `ERR_STAY_TOO_FAR` |
| İstek başına yolcu | 1–20 | `ERR_NO_TRAVELLERS`, `ERR_TOO_MANY_TRAVELLERS` |
| Yaş | 0–120 | `ERR_INVALID_AGE` |

### 1.4 Para
<!-- de:3186200e07be -->

- Tüm tutarlar **tam sent** cinsindendir (`…Cents`, tam sayı). 18000 = 180,00.
- **Yuvarlama:** ticari yuvarlama ile 2 basamağa, **yolcu başına bir kez** (her
  fiyat yanıtında `rounding`: `{"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"}` – otelin
  EDF'sinin beyan ettiği kuralın aynısı). Bir yolcunun kalemleri tam olarak
  (yuvarlanmadan) toplanır, yalnızca toplam yuvarlanır: bu `perTravellerCents[i]` değeridir,
  `totalCents` bunların toplamıdır. „89,90 üzerinden −%15“ gibi bir kalem −13,485'tir ve
  toplama kadar öyle kalır. Kalem başına tam tutarı `breakdown[].amountExact` gösterir (Bölüm 3.1).
- Para birimi otelin **sözleşme para birimidir** (`currency`, ISO 4217). TourAPI döviz
  çevirmesi yapmaz. İstekteki `currency` bir tercihtir: farklıysa yanıt
  sözleşme para biriminde, `warnings` içinde bir uyarıyla gelir. Sözleşmede para birimi tanımlı değilse
  `currency` boş kalır, yine bir uyarıyla – „EUR“ tahmin edilmez.
- `priceCheck` ile `/v1/book` çağrısında ise para birimi çelişkisi bir hatadır
  (`ERR_CURRENCY_NOT_AVAILABLE`): ortak birimi olmayan tutarlar karşılaştırılmaz.

### 1.5 Konaklama düzeni
<!-- de:c5a60936f983 -->

`occupancy.travellers`, **bir odanın** yolcularının listesidir; her yolcu için
giriş günündeki `age`. `name` ve `type` gönderilebilir, ancak fiyata etkisi
yoktur. Çocuk fiyatları, tam ücret ödeyenler ve minimum doluluk sözleşmeden çıkar:

- Birinin çocuk mu yetişkin mi olduğuna sabit bir sınır değil, odanın çocuk yaş aralığı karar verir (ör. 2–11
  yaş: 14 yaşındaki biri yetişkin fiyatı öder).
- Bir oda belirli sayıda tam ücret ödeyen isteyebilir (ör. çift kişilik odada 2). Daha az
  yetişkin seyahat ederse, boş kalan tam ücret yerini bir çocuk doldurur ve **tam
  temel fiyatı öder** (pansiyon tipi çocuk tarifesiyle); çocuk indirimleri ancak bunun
  ötesindeki çocuklar için geçerlidir.
- „1. çocuk / 2. çocuk“ gibi kademeler, çocukları otelin belirlediği sırayla sayar
  (önce en büyük veya önce en küçük). Aynı sıra, hangi çocuğun boş bir
  tam ücret yerini dolduracağını da belirler.
- Çocuk yaş aralığından küçük olan bebektir: kişi başı fiyatta temel fiyat olmadan, asla
  bir tam ücret yerinde değil; pansiyon tipi veya ayrı bir bebek fiyatı yalnızca
  sözleşme bunları belirtiyorsa. „3 kişiden itibaren“ tekliflerinde bir bebek, yalnızca oda
  onu doluluğa sayıyorsa sayılır.
- Hiçbir yolcu 0'dan az ödemez. Yüzde indirimleri, yolcunun çocuk veya kişi
  indiriminden sonra borçlu olduğu tutar üzerinden hesaplanır (ücretsiz bir çocuk ücretsiz kalır,
  −%50 çocuk fiyatında −%20'lik bir erken rezervasyon indirimi yarısına uygulanır).

### 1.6 Yanıtlar, hatalar, uyarılar
<!-- de:0d055d711a92 -->

- Başarı: sonuçla birlikte `200`. Hata: `4xx`/`5xx` ile

  ```
  {"errorCode": "ERR_…", "message": "…", "warnings": ["…"]}
  ```

  `errorCode` kararlıdır ve programlar için tasarlanmıştır, `message` insanlar için Almanca bir metindir
  ve değişebilir. Tam liste: Bölüm 7.
- `warnings` (isteğe bağlı, başarılı yanıtlarda da): API'nin istekte fark ettiği,
  ancak reddetmediği şeyler – yok sayılan alanlar, farklı para birimi, yok sayılan `now`.
  **Lütfen loglayın.** Birçok entegrasyon hatası tam olarak orada görünür.
- **İşlem süresi:** Bir uç noktanın her yanıtı – hata yanıtları ve `/v1/health` dahil –
  `Server-Timing: tourapi;dur=<ms>` başlığını taşır (W3C Server Timing), örn. `tourapi;dur=12.4`:
  isteğin uç noktaya ulaşmasından yanıtın başlamasına kadar geçen milisaniye; istek gövdesinin
  okunması ile hız sınırı ve arama kapısında bekleme (Bölüm 8) dahil, yanıtın aktarımı hariç.
  Kendi ölçtüğünüz süreyle arasındaki fark ağ ve aktarımdır. Değer bilgi amaçlıdır, bir taahhüt
  değildir; ihtiyacınız yoksa başlığı yok sayın.
- **Bilinmeyen alanlar:** Sorgu isteklerinde (`/v1/price`, `/v1/prices`, `/v1/search`)
  bilinmeyen bir alan yok sayılır ve `warnings` içinde belirtilir. `occupancy` içinde
  ise reddedilir (`422 ERR_UNKNOWN_FIELD`), çünkü orada bir yazım hatası fiyatı değiştirir.
  `/v1/book` ve `/v1/cancel` bilinmeyen **her** alanı reddeder.

### 1.7 Örneklerin test dünyası
<!-- de:babe2802fad0 -->

Örnekler işletmecinin test dünyasına (Golden Seed) karşı çalışır: tur operatörü
`TEST-TENANT-A`, oteller `TEST-HOTEL-…`, odalar `DZ`/`EZ`, pansiyon tipleri `RO`/`BB`/`HB`. Yer tutucular:

| Yer tutucu | Anlamı |
|---|---|
| `{{baseUrl}}` | Temel URL |
| `{{apiKey}}` | **Müşteri grubu olmayan** anahtar (temel sözleşme). |
| `{{rabattKey}}` | `TEST-PARTNER-DISCOUNT` müşteri grubunun anahtarı (`TEST-HOTEL-DISCOUNT` üzerinde −%20, fiyat grubu) |
| `{{partnerKey}}` | `TEST-PARTNER-BASE` kontenjan grubunun anahtarı |
| `{{poolKey}}` | `TEST-PARTNER-POOL` kontenjan grubunun anahtarı, dışa aktarım yetkisi **olmadan** |
| `{{gesperrterKey}}` | iptal edilmiş anahtar |
| `{{exportEpoch}}`, `{{exportSeq}}` | `export-voll` örneğinin manifestindeki `epoch` ve `to_seq` |
| `{{ohnePreisRef}}`, `{{zweiteRef}}` | Sırasıyla `buchen-ohne-preispruefung` ve `buchen-gleiche-kundenreferenz` örneklerinden rezervasyon referansları |
| `{{D0}}`, `{{D2}}`, … | Test dünyasının giriş günü D0 artı n gün. D0 = test verilerinin ilk kurulduğu gün + 30 gün. D0 yalnızca test verilerinin sabit bir takvim günüdür, 1.3'teki referans tarihi **değildir** (o her zaman sunucu tarihidir) |
| `{{heute}}`, `{{gestern}}` | Sunucu tarihi (= referans tarihi), önceki gün |
| `{{buchungsRef}}` | `buchen` örneğinden rezervasyon referansı |
| `{{feedToken}}`, `{{inhaltCursor}}` | `inhalt-verzeichnis` örneğinden `feedToken` ve `nextCursor` |
| `{{inhaltEtag}}` | `inhalt-hotel` örneğinden `ETag` |
| `{{*}}` | (yalnızca yanıtlarda) herhangi bir değer, ör. zaman damgası |

Test dünyasının fiyatları (DZ, 2 yetişkin, yalnızca oda): ilk gece 100,00, sonraki her
gece oda başına 80,00; kahvaltı +20,00, yarım pansiyon kişi ve gece başına +35,00.
İstisna `TEST-HOTEL-KIND` (hedef `ACE`): kişi ve gece başına fiyat (DZ 89,90 / 79,90),
2 ile 11 yaş arası çocuklar −%15.

### 1.8 Sağlık: `GET /v1/health`
<!-- de:45efe8eeb5b4 -->

Load balancer'lar ve izleme için, **anahtarsız**. `200 {"status": "ok"}` şu anlama gelir: Düğüm
stoku yüklemiştir ve veritabanıyla son eşitlemesi tazedir (varsayılan: 60 sn'den
yeni). Aksi halde `status` `degraded` (eşitleme çok eski, `reason: "sync_stale"`) veya
`down` (stok yüklenmemiş, `reason: "view_not_loaded"`) ile `503`, ayrıca metin olarak `detail`. Yanıt
tur operatörü veya stok verisi içermez. Bir gönderici IP'sinden saniyede 10'dan fazla
çağrı gecikmeli yanıtlanır (en fazla 1 sn, Bölüm 8).

Düğüm bir okuma replikası üzerinden okuyorsa, yanıt ayrıca onun gecikmesini
saniye cinsinden belirtir (`replica_lag_s`). 5 sn'nin üzerinde durum `ok` kalır, `warnings` o zaman
`"replica_lag"` içerir. `503 degraded`, eşitleme yaşı artı gecikme sınırı
aştığında (`reason: "replica_lag"`) veya gecikme ölçülemediğinde gelir
(`reason: "replica_lag_unknown"`).

Her yanıt (`503` dahil) ayrıca sürecin çalışma süresini saniye cinsinden
(`uptime_s`) ve derleme durumunu (`version`, durumu olmayan bir derlemede boş) belirtir.
Düğüm veritabanını ölçer ölçmez `db` yer alır: `db_latency_ms` ile `"ok"` (bir gidiş-dönüşün
süresi, health çağrısında değil eşitleme döngüsünde ölçülür) veya `"error"` (son
ölçüm başarısız). `"error"` tek başına durumu değiştirmez — düğüm yüklenmiş
stokundan yanıt verir; `ok` durumunda `warnings` içinde `"db_error"` yer alır ve
eşitleme gerçekleşmezse, sınırdan sonra `503 degraded` (`sync_stale`) gelir.

`warnings` içindeki `"capacity_overlap"` (durum `ok` kalır) şu anlama gelir: Eşitleme döngüsündeki
stok kontrolü, oda tipi başına çakışan kapasite dönemleri bulmuştur — bir
işletmeci bulgusu (stok, yazma yolu dışından değiştirilmiş), isteğin hatası değil.

```http
### gesundheit
GET {{baseUrl}}/v1/health
```

```json
{
  "status": "ok",
  "db": "ok",
  "db_latency_ms": "{{*}}",
  "uptime_s": "{{*}}",
  "version": "{{*}}"
}
```

### 1.9 `/v1` için uyumluluk taahhüdü
<!-- de:e05b432844b4 -->

`/v1` içinde API yalnızca **eklemeli** olarak değişir:

| `/v1` içinde gelebilir | `/v1` içinde asla gelmez (bu `/v2` olurdu) |
|---|---|
| yeni isteğe bağlı istek alanları | istekte yeni zorunlu alanlar |
| yeni yanıt alanları | alanları kaldırmak veya yeniden adlandırmak |
| yeni endpoint'ler | bir alanın tipini veya anlamını değiştirmek |
| yeni `ERR_*` kodları, her biri hata kataloğunda belgelenmiş ele alma yöntemiyle | mevcut bir `ERR_*` kodunun anlamını değiştirmek |

Alıcının bunun için yapması gerekenler:

- **Bilinmeyen yanıt alanlarını yok sayın**, reddetmeyin.
- **Bilinmeyen `ERR_*` kodlarını** HTTP durumuna göre ele alın (`4xx`: körü körüne
  tekrarlamayın, `5xx`/`429`: aralıklı tekrarlayın, Bölüm 7).
- `message` ve `warnings` insanlar için metinlerdir ve taahhüdün parçası değildir.

`/v1` içinde sabit: zaman damgaları UTC olarak RFC 3339'dur (`Z`), tutarlar tam sent (1.4),
metin uzunlukları yazmadan önce kontrol edilir (alan adıyla `422`, asla `500`, Ek),
`/v1/health` anahtar gerektirmez (1.8).

### 1.10 İş ortağı portalına giriş
<!-- de:e1506791fd15 -->

Portal (dokümantasyon, kendi erişimleriniz ve anahtarlarınız) API'nin parçası değildir; API
yalnızca anahtara ihtiyaç duyar. Portala giriş şöyle işler:

- Portal erişimini tur operatörü oluşturur. Bir kullanıcı adı ve **tek kullanımlık şifre**
  (30 gün geçerli) alırsınız.
- **İlk giriş:** kullanıcı adı ve tek kullanımlık şifre, ardından **ikinci faktörü** kurun
  (RFC 6238 uyumlu doğrulayıcı uygulama: QR kodunu tarayın veya anahtarı yazın, altı haneli kodu
  girin), sonra kendi şifrenizi belirleyin. Geçerli bir tek kullanımlık şifre olmadan ikinci
  faktör kurulamaz.
- **Sonraki her giriş:** şifre ve güncel kod. Bir kod yalnızca bir kez geçerlidir.
- **Yanlış kodlar:** 5 yanlıştan sonra giriş sona erer. 7 gün içinde 10 başarısız deneme erişimi
  kilitler, doğru kod için de. Kilidi yalnızca tur operatörü açabilir.
- **Kilitlendi veya telefon kayboldu:** tur operatörüne başvurun. „Kilitlenmeyi kaldır“ erişimi
  mevcut ikinci faktörle yeniden açar. „2. faktörü sıfırla“ yeni bir tek kullanımlık şifre verir;
  ardından ikinci faktörü ve şifrenizi yeniden kurarsınız, eski şifre artık geçerli değildir.
- Portal ve tur operatörü konsolu ayrıdır: portal erişim bilgileri konsolda geçerli değildir.

---

## 2. Bir entegrasyonun akışı
<!-- de:8bccf11eafcc -->

```
/v1/destinations
  -> /v1/search
  -> /v1/price | /v1/prices
  -> /v1/book        idemKey, priceCheck
  -> /v1/booking
  -> /v1/cancel      idemKey
```

- `/v1/destinations`: anahtarın geçerli `destination` kodları.
- `/v1/search`: başlangıç fiyatı ve müsaitlik bilgisiyle rezerve edilebilir oteller.
- `/v1/price`: otel, oda, pansiyon ve kişi dağılımı için fiyat bilgisi (bağlayıcı olan yalnızca
  `/v1/book`); `/v1/prices`: tüm pansiyon tipleri (ve odalar) tek seferde.
- `/v1/book`: `idemKey` ve `priceCheck` (az önce alınan fiyat) ile rezervasyon yapar.
- `/v1/booking`: rezervasyonu bizim referansımız veya kendi referansınız üzerinden okur.
- `/v1/cancel`: aynı `idemKey` üzerinden iptal eder.

Bilinmesi gereken kurallar:

1. **Arama ve fiyat bilgi amaçlıdır, bağlayıcı olan yalnızca `/v1/book`'tur.** Fiyat ile
   rezervasyon arasında tur operatörü fiyatları veya kontenjanı değiştirebilir. `priceCheck` ile
   `/v1/book` değişmiş bir fiyatı, sessizce yeni fiyattan rezervasyon yapmak yerine reddeder
   (`409 ERR_PRICE_DRIFT`).
2. **Her rezervasyonun kendi `idemKey` değeri vardır** (çağıran tarafından verilir, ör. kendi
   işlem numarası). Aynı anahtarla yapılan tekrarlar asla çift rezervasyon oluşturmaz (Bölüm 9).
3. **Hataları körü körüne tekrarlamayın.** Hangi hataların tekrarlanmaya değdiği
   hata kataloğunda (sütun „Çağıran“) yer alır.

---

## 3. Fiyat: `/v1/price` ve `/v1/prices` (BA)
<!-- de:4e22b7e304e6 -->

### 3.1 `POST /v1/price` – tek fiyat
<!-- de:6ff33502f929 -->

Bir otelin tam olarak bir odasını bir konaklama, bir pansiyon tipi ve bir
konaklama düzeni için fiyatlandırır.

| Alan | Zorunlu | Anlamı |
|---|---|---|
| `hotel` | evet | Otel kodu |
| `room` | hayır | Oda kodu. Yoksa API, konaklama düzenine izin veren ve pansiyon tipini sunan **en uygun fiyatlı müsait** odayı alır; hiçbiri müsait değilse bunların en uygun fiyatlısını (o zaman `available=false`, 3.3). Sözleşme verileri hesaplama çekirdeği tarafından reddedilen bir oda (7.5, ör. `ERR_INVALID_AMOUNT`, `ERR_NO_SECTION`) bu sırada atlanır ve `warnings` içinde oda ve kodla belirtilir; hiçbir oda hesaplanamazsa hata 422 olarak gelir. `room` ile bu odanın hatası her zaman 422 olarak gelir. |
| `board` | evet | Pansiyon tipi kodu, tam olarak sözleşmedeki gibi (`RO`, `ro` değil). Eksikse: `ERR_BOARD_MISSING`; oda (`room` olmadan: hiçbir oda) bunu sunmuyorsa: `422 ERR_BOARD_NOT_OFFERED` |
| `checkIn`, `checkOut` | evet | Konaklama (Bölüm 1.3) |
| `occupancy.travellers[]` | evet | `age` ile yolcular |
| `currency` | hayır | Tercih edilen para birimi, yalnızca uyarı (1.4) |
| `now` | hayır | tolere edilir, yok sayılır (1.3) |

Yanıt:

| Alan | Anlamı |
|---|---|
| `room` | fiyatlandırılan oda (istekte `room` yoksa: en uygun fiyatlı müsait oda); `/v1/book`'a bu şekilde aktarın |
| `currency` | Sözleşme para birimi, boş = sözleşmede tanımlı değil |
| `totalCents` | Odanın konaklama için toplam fiyatı |
| `rounding` | Yanıtın yuvarlama kuralı (Bölüm 1.4) |
| `perTravellerCents[]` | Yolcu başına fiyat, **yaşa göre azalan** sırada (önce en büyük, aynı yaşta istek sırası): kalemlerinin tam toplamı, bir kez yuvarlanmış. Toplam = `totalCents`. Oda fiyatlarında (obje fiyatı) oda kalemlerini ilk yolcu taşır, pansiyon tipi ilgili yolcuda yer alır. |
| `breakdown[]` | Tekil kalemler: `chargeType` (`BaseCharge`, `PhantomBaseCharge`, `GuestCharge`, `BoardCharge`, `Extra`), `code`, `traveller` (`perTravellerCents` içindeki indeks), `night` (0'dan başlayan indeks, `-1` = konaklama başına), `amountExact` (ondalık sayı olarak tam tutar, ör. `"-13.485"`), `amountCents`, ekstralarda `applianceCode`, bir ekstra ailesinin ekstralarında (aynı `applianceCode` değerine sahip birden fazla ekstra) ayrıca `variant` (varyant; ekstrayı ancak ikisi birlikte belirtir), ücretsiz gece kalemlerinde ayrıca `freeNight: true` (bu gece tamamen veya kısmen bağışlanmıştır, ör. „7=6“: yedinci gece yolcu başına temel fiyatı ve – teklife göre – pansiyon tipini sıfırlayan bir kalem taşır). Sıra: gece, onun içinde yolcu, onun içinde hesaplama adımı (temel fiyat, kişi indirimi/ek ücreti, kendi kişi indirimiyle birlikte pansiyon tipi, ekstralar); konaklama başına kalemler en sonda. |
| `separateExtras[]` | yalnızca varsa: ayrı gösterilen ekstralar. `inTotal=true`: zorunlu ekstra, toplam fiyata dahil. `inTotal=false`: isteğe bağlı ekstra, toplam fiyata **dahil değil**. `code` ekstrayı belirtir, bir ekstra ailesinde `variant` ile birlikte. `amountCents` ekstranın tam toplamıdır, bir kez yuvarlanmış; `inTotal=true` durumunda kendi `breakdown` satırlarının toplamından birkaç sent sapabilir – belirleyici olan `breakdown`'dur. |
| `availability` | `configured` (kontenjan tanımlı), `available` (her gece açık), `minFree` (geceler boyunca en küçük boş sayı, 99'a kadar tam; `-1` = her gece 99'dan fazla boş veya serbest satış; `0` = en az bir gece rezervasyona kapalı) |
| `warnings[]` | Uyarılar (1.6); `room` olmadan ayrıca sözleşme hatası nedeniyle atlanan her oda için: `zimmer 'EZ' ausgelassen: ERR_INVALID_AMOUNT (…)` |

```http
### preis-einzeln
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "currency": "EUR",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "room": "DZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 18000,
  "perTravellerCents": [18000, 0],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "100.00", "amountCents": 10000},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "80.00", "amountCents": 8000}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5}
}
```

`room` olmadan API en uygun fiyatlı uygun odayı arar – burada bir kişi için EZ;
yanıttaki `room` onu belirtir.

```http
### preis-guenstigstes-zimmer
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{
  "room": "EZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 12500,
  "perTravellerCents": [12500],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "70.00", "amountCents": 7000},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "55.00", "amountCents": 5500}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5}
}
```

Dolu, kapatılmış, kontenjan tanımlanmamış veya – bir müşteri grubunun anahtarında –
tahsis edilmemiş odaları `room` olmadan yapılan seçim, daha ucuz olsalar bile atlar: Aşağıdaki otelde EZ'nin (70 EUR) kontenjanı yoktur
ve rezervasyona kapalıdır, API daha pahalı olan, rezerve edilebilir DZ'yi alır.

```http
### preis-guenstigstes-verfuegbares
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-STOP",
  "board": "RO",
  "checkIn": "{{D10}}",
  "checkOut": "{{D11}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{
  "room": "DZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 10000,
  "perTravellerCents": [10000],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "100.00", "amountCents": 10000}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5}
}
```

**Oda fiyatlarında kişi başı fiyat.** Birçok sözleşme kişiyi değil odayı fiyatlandırır
(obje fiyatı). Bu durumda tüm oda kalemleri – temel fiyat, oda veya işlem başına ekstralar –
`perTravellerCents` içinde ilk yolcunun kalemine yazılır, diğerleri bunun karşılığında `0` taşır
(`preis-einzeln` örneğinde: `[18000, 0]`). Pansiyon tipi, obje fiyatında da her zaman kişi ve gece başına
bir fiyattır: ilgili yolcuda yer alır (kahvaltı ile `[22000, 4000]`,
bkz. `preis-alle-verpflegungen`); tek başına seyahat eden kişi tam oda fiyatını, ancak yalnızca
bir pansiyon tipini öder. Kişi başına sabit bir indirim (ör. kişi başına −20 € erken rezervasyon) obje
fiyatında oda fiyatını düşürür ve bu nedenle yine ilk kalemde yer alır. Bu kasıtlıdır ve böyle kalır
(EDF 5.1.6: „objektbasierte Zusatzleistungen werden der 1. Person zugerechnet“; her kalem
bir kez yuvarlanır, toplam her zaman tam olarak `totalCents` olur). İlk kalem **en büyük**
yolcuya aittir – `perTravellerCents` ve `breakdown[].traveller` isteğin sırasına göre değil,
yaşa göre azalan sırada dizilir. Kişiye bağlı kalemler (ör.
çocuk fiyatları) ilgili yolcuda yer alır. Bu nedenle `perTravellerCents` „kişi başı“
gösterim için tasarlanmamıştır: `totalCents` değerini yolcu sayısına bölün
ve kendiniz yuvarlayın.

Fiyat anahtara bağlıdır: Aynı istek bir müşteri grubunun anahtarıyla o grubun
özel fiyatını verir (burada −%20). `TEST-PARTNER-DISCOUNT` bir fiyat grubudur: genel
stoktan rezervasyon yapar, `availability` grubu olmayan anahtardakiyle aynıdır (1.1).

```http
### preis-kundengruppe
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{rabattKey}}

{
  "hotel": "TEST-HOTEL-DISCOUNT",
  "room": "DZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "room": "DZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 14400,
  "perTravellerCents": [14400, 0],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "80.00", "amountCents": 8000},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "64.00", "amountCents": 6400}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5}
}
```

Çocuk indirimli kişi başı fiyat: 89,90 üzerinden −%15, −13,485 eder – bir sentin
kesri. Kalem tam kalır (`amountExact`); yalnızca çocuğun toplamı bir kez yuvarlanır:
89,90 − 13,485 + 79,90 − 11,985 = 144,33 → `perTravellerCents[2]` = 14433. Bir yolcunun `amountCents`
değerleri, tam olarak onun fiyatını verecek şekilde dağıtılır: her satır, yolcunun o satıra kadarki
yuvarlanmış kümülatif toplamı eksi bir önceki satıra kadarki toplamdır
(8990, −1348, 7990, −1199 = 14433). Kalemleri tek tek yuvarlayıp toplayan (−13,49, −11,99)
144,32'ye ulaşır – bu fiyat değildir.

```http
### preis-kind-bruchcent
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-KIND",
  "room": "DZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}, {"age": 8}]}
}
```

```json
{
  "room": "DZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 48393,
  "perTravellerCents": [16980, 16980, 14433],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "89.90", "amountCents": 8990},
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 1, "night": 0, "amountExact": "89.90", "amountCents": 8990},
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 2, "night": 0, "amountExact": "89.90", "amountCents": 8990},
    {"chargeType": "GuestCharge", "code": "Base", "traveller": 2, "night": 0, "amountExact": "-13.485", "amountCents": -1348},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "79.90", "amountCents": 7990},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 1, "night": 1, "amountExact": "79.90", "amountCents": 7990},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 2, "night": 1, "amountExact": "79.90", "amountCents": 7990},
    {"chargeType": "GuestCharge", "code": "ExtraDay", "traveller": 2, "night": 1, "amountExact": "-11.985", "amountCents": -1199}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5}
}
```

**Ücretsiz geceler („7=6“).** Bir geceyi bağışlayan kalem, teklifin
`applianceCode` değeri ve `freeNight: true` ile bir `Extra`'dır; toplam fiyata dahildir ve
bağışlanan gecede her yolcu için doğrudan onun temel fiyatının arkasında yer alır. Test dünyasında
(`TEST-HOTEL-FREINACHT`, DZ, yalnızca oda, kişi başı fiyat: ilk gece 100,00, sonraki her gece
80,00, `checkIn` = `{{D0}}`) 2 yetişkin 7 gece için 1160,00 yerine 1000,00 öder: gece 6
(yedinci gece) her yolcu için `{"chargeType": "Extra", "code": "PN", "night": 6, "amountExact":
"-80.00", "applianceCode": "FSO1", "freeNight": true}` taşır. Yarım pansiyonla aynı kalem
pansiyon tipini de bağışlar (`-115.00`). 14 gece iki ücretsiz gece verir (gece 12 ve 13), 6 gece
hiç vermez.

### 3.2 `POST /v1/prices` – tüm pansiyon tipleri tek seferde
<!-- de:6d0181b133e5 -->

`/v1/price` gibi, ancak `board` yerine isteğe bağlı `boards[]` (yoksa: odanın tüm pansiyon
tipleri) ve isteğe bağlı `room` (yoksa: tüm odalar). Konaklama düzenine izin
vermeyen odalar düşer; hiçbiri izin vermiyorsa neden hata olarak gelir
(`ERR_OCCUPANCY_NOT_ALLOWED`). Hiçbir odanın sunmadığı, istenen bir pansiyon tipi:
`422 ERR_BOARD_NOT_OFFERED`. `board` alanı burada filtreleme **yapmaz**: Onu gönderen
yine de tüm pansiyon tiplerini ve `warnings` içinde bir uyarı alır (aşağıdaki örnek);
filtreleme yalnızca `boards[]` ile yapılır.

Yanıt: `currency`, `rounding`, `room` ve `boards[]` içeren `rooms[]` (her pansiyon tipi için `board`,
`globalType` (EDF dışa aktarımındaki ve açık aramadaki pansiyon türü: `RO` için `AO`, aksi halde tur operatörünün eşlemesi, aksi halde bir pansiyon türüyse kodun kendisi, aksi halde `XX` = eşlenemez), `totalCents`, `perTravellerCents`, `separateExtras`,
`availability`), oda başına fiyata göre artan sırada; `warnings`. `breakdown` yok.

Bir odanın bir pansiyon tipi bu konaklama düzeni için hesaplanamıyorsa, çünkü sözleşme orada
hatalıdır (`ERR_NEGATIVE_TRAVELLER_PRICE`, `ERR_NEGATIVE_PERCENT_BASE`, Bölüm 7.5)
veya bu yolcu grubuna satılmıyordur (`ERR_BOARD_NOT_AVAILABLE`, 3.5),
yalnızca bu kombinasyon düşer: fiyatsız olarak `rooms[].errors[]` içinde (`board`,
`globalType`, `errorCode`, `message`) ve `warnings` içinde yer alır; matrisin geri kalanı fiyatlanmış kalır,
odanın `boards[]` listesi o zaman boş olabilir. Bir odanın diğer her sözleşme hatasında da
aynısı geçerlidir (Bölüm 7.5, ör. `ERR_NO_SECTION`: bir gece için sözleşmede sezon yok):
etkilenen pansiyon tipleri kodla birlikte `rooms[].errors[]` ve `warnings` içinde yer alır,
diğer tüm odalar `room` ve `board` ile `/v1/price` ile aynı fiyatlarla fiyatlanmış kalır.
Tek bir kombinasyon bile hesaplanamıyorsa kod hata olarak gelir (422). İsteğin kendisindeki
hatalar isteği yine tamamen reddeder.

**Süre sınırı:** `/v1/prices` ve `room` olmadan `/v1/price` birden fazla odayı veya
pansiyon tipini hesaplar; bu 2 sn'yi aşarsa (normal işletimde birkaç milisaniye sürer)
`503 ERR_PRICE_TIMEOUT` gelir – asla yarım bir matris veya odaların bir kısmından seçilmiş
„en uygun fiyatlı“ bir oda gelmez. Çözüm: `room` veya `boards` belirtin. Süre sınırı `/v1/prices` için her zaman geçerlidir,
`room` ve tek bir pansiyon tipiyle de; yalnızca `room` **ile** `/v1/price` çağrısının kendi
süre sınırı yoktur ve bu nedenle güvenli çıkış yoludur. `503`, `Retry-After` taşımaz: önce daraltın,
sonra tekrarlayın. Çağıran bağlantıyı kapatırsa
hesaplama iptal edilir ve yanıt verilmez.

```http
### preis-alle-verpflegungen
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "rooms": [
    {
      "room": "DZ",
      "boards": [
        {"board": "RO", "globalType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
         "availability": {"configured": true, "available": true, "minFree": 5}},
        {"board": "BB", "globalType": "BB", "totalCents": 26000, "perTravellerCents": [22000, 4000],
         "availability": {"configured": true, "available": true, "minFree": 5}},
        {"board": "HB", "globalType": "HB", "totalCents": 32000, "perTravellerCents": [25000, 7000],
         "availability": {"configured": true, "available": true, "minFree": 5}}
      ]
    }
  ]
}
```

`boards[]` yerine `board` ile, onsuz halle aynı matris gelir, ayrıca şu uyarı
`board: 'HB' wird bei /v1/prices ignoriert — Verpflegungen ueber boards waehlen (fehlt boards:
alle des Zimmers)`:

```http
### preis-alle-verpflegungen-board-ignoriert
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "board": "HB",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

### 3.3 Fiyat yanıtlarında müsaitlik
<!-- de:b89228c85f35 -->

`availability` her zaman doludur ve fiyatlandırılan odayı tüm geceler boyunca ifade eder.
`available=false` olan bir fiyat bir **fiyat bilgisidir, teklif değildir**: rezervasyon
başarısız olur (Bölüm 5.3). Tam olarak `/v1/book`'un karşısında satış yaptığı şey sayılır: rezervasyonlar ve
günlük durumdan sonra gece başına boş kapasite ve bir kontenjan grubunun anahtarında
ek olarak onun tahsisi (grubun zaten rezerve ettiği düşülmüştür; oda ve gece için tahsis
yoksa hiçbir şey boş değildir). Bir fiyat grubu, grubu olmayan bir anahtar gibi sayılır (1.1). `minFree` bunların geceler boyunca en küçüğüdür.

TourAPI gece başına 99'a kadar tam sayar. Hangi gecenin `minFree`'ye neyle katkıda bulunduğu:

| Gece | `minFree`'ye katkı | Allotment dosyası (Bölüm 10) |
|---|---|---|
| 1 ile 99 arası birim boş | sayının kendisi | `01`–`99` |
| 99'dan fazla boş veya serbest satış | `minFree`'yi düşürmez | `**` |
| Satış durdurma | `0` | `SS` |
| talep üzerine | `0` | `RR` |
| dolu, kapalı, kapasitesiz, oda ve gece için tahsisi olmayan müşteri grubu | `0` | `00` |

`minFree` yalnızca **her** gecede 99'dan fazla boş yer varsa veya serbest satıştaysa `-1`'dir;
satış durdurma, talep üzerine veya `0` olan tek bir gece `minFree`'yi `0` yapar (`available=false`). Bu,
önbellek teslimatının EDF allotment'ındaki gösterimle aynıdır, böylece önbellek ve API
aynı sayıyı gösterir.

- `configured=true`: TourAPI bu oda için bir kontenjan biliyor. Bu henüz her bir gece
  hakkında bir şey söylemez: Kapasite girilmemiş bir gece
  odayı `available=false` yapar (`minFree` 0), ancak oda `configured=true` kalır.
- `configured=false`: bu oda için hiç kontenjan tanımlı değildir; rezervasyon yapılamaz.

Yalnızca tek tek gecelerde kapasite eksikse, arama ve `/v1/book` aynı nedeni belirtir:
`ERR_NO_INVENTORY` (arama 4.2, rezervasyon 5.3). „Rezerve edilebilir“ için belirleyici olan `configured` değil,
`available`'dır.

`availability` bir anlık durumdur, taahhüt değildir; bağlayıcı kontrolü yalnızca `/v1/book` yapar. Bir rezervasyonu
veya iptali, onu gerçekleştiren sunucu düğümü bir sonraki istekte gösterir
(`minFree` düşer veya yükselir). Diğer düğümler, diğer kanalların rezervasyonları ve sözleşme değişiklikleri
birkaç saniye içinde yansır – aynı şekilde kendi düğümü de, yeni durumu hemen yükleyemediği
nadir durumda (rezervasyonun kendisi yine de geçerlidir).

### 3.4 Odanın satış kuralları: minimum konaklama, giriş günleri, satış penceresi, release
<!-- de:0807e2cfe292 -->

Bir otel sözleşmesi, otelin hangi konaklamaları kabul ettiğini belirleyebilir, örneğin
„yüksek sezonda en az 7 gece, giriş yalnızca cumartesi“, „yarım pansiyon yalnızca
31.10.'a kadar girişte“ veya „release 14 gün“ (rezervasyon en geç girişten 15 gün önce). Bu tür kurallar fiyatı değiştirmez; bir odanın istenen
konaklama için sunulup sunulmayacağına **karar verir**. Bir kuralın hariç tuttuğu konaklama
fiyat almaz:

| Kod | Kural şunu gerektirir … | Çağıran |
|---|---|---|
| `ERR_STAY_LENGTH_NOT_ALLOWED` | farklı bir konaklama süresi (minimum veya maksimum gece) | süreyi değiştirin |
| `ERR_ARRIVAL_DAY_NOT_ALLOWED` | farklı bir giriş veya çıkış haftanın günü | seyahat günlerini kaydırın |
| `ERR_TRAVEL_DATES_NOT_ALLOWED` | belirli bir dönem içinde seyahat tarihleri (satış penceresi) | farklı dönem |
| `ERR_BOARD_NOT_ALLOWED` | bu konaklama için farklı bir pansiyon tipi | farklı pansiyon tipi |
| `ERR_LEAD_TIME_NOT_ALLOWED` | daha uzun ön süre: giriş release süresi içinde | daha geç giriş |

`message` odayı, kuralı ve gerekeni belirtir, örneğin „Zimmer DZ, Verkaufsregel 1: wenn
Anreise 2026-07-01 bis 2026-08-31, verlangt mindestens 7 Nächte“. Bir konaklama bir kuralın
birden fazla kısmını ihlal ederse şu sıra geçerlidir: süre, haftanın günü, dönem, pansiyon tipi,
release.

**Release (ön süre).** n günlük release içeren bir kural, referans tarihi (sunucu tarihi,
1.3) ile giriş arasında n günden **fazla** takvim günü gerektirir: release 3 ve referans
tarihi 10.05. ise 13.05. girişi hâlâ kapalıdır, 14.05.'ten itibaren rezerve edilebilir; release
0 ise yalnızca referans tarihindeki giriş kapalıdır. Release bir döneme bağlı olabilir (örneğin
„eylülden itibaren 14 gün“): konaklamanın bir gecesi o dönemdeyse geçerlidir, girişe
kadar ölçülür. Kural çıkışa bağlıysa (`ApplyTo` `Departure`), süre çıkışa kadar ölçülür:
bu durumda referans tarihi ile çıkış arasında n günden fazla takvim günü gerekir. `message` referans tarihini, girişi ve ön süreyi belirtir. Bugün kapalı olan bir
konaklama yarın açılmaz; tekrar denemek işe yaramaz.

- `/v1/price`: `room` olmadan, kuralı konaklamayı hariç tutan bir oda
  seçimden düşer; hiçbir oda konaklamayı kabul etmezse neden 422 olarak gelir. Bu neden, başka bir odanın
  `ERR_OCCUPANCY_NOT_ALLOWED` hatasına göre önceliklidir, çünkü neyin değiştirilmesi gerektiğini daha kesin söyler.
- `/v1/prices`: kurallar pansiyon tipi başına geçerli olabilir; yalnızca ilgili
  pansiyon tipi düşer. Geriye bir şey kalmazsa neden 422 olarak gelir.
- `/v1/search`: otel düşer, kod `diagnostics.reasons` içinde yer alır.
- `/v1/book`: `priceCheck` ile kural istenen pansiyon tipi için geçerlidir; `priceCheck`
  olmadan (pansiyon tipi bilinmiyor), odanın bir pansiyon tipi konaklamayı kabul eder etmez
  satış yapılır.

`ERR_ROOM_RESTRICTION_INVALID` (422) şu anlama gelir: sözleşmedeki bir kural değerlendirilemiyor. Bu
bir sözleşme hatasıdır – bildirin.

### 3.5 Yalnızca belirli yolcu grupları için pansiyon tipi
<!-- de:a30ca6afd1dc -->

Bir pansiyon tipi ek ücreti yolcu grubunun bileşimine bağlı olabilir, örneğin
„yarım pansiyon ek ücreti en az 2 yetişkinli gruplar için geçerlidir“. İstenen
konaklama düzeni buna uymuyorsa, bu pansiyon tipi bu konaklama düzeni için **rezerve edilemez**:
`422 ERR_BOARD_NOT_AVAILABLE`. TourAPI hiçbir zaman bir pansiyon tipi için pansiyon tipi olmadan
fiyat hesaplamaz. `message` pansiyon tipini, yolcuyu, geceyi ve gereken grubu belirtir. Bu
bir sözleşme hatası değildir; farklı bir pansiyon tipi veya konaklama düzeni hesaplanabilir (Çağıran: farklı
pansiyon tipi veya konaklama düzeni).

- `/v1/price`: `room` olmadan oda bu pansiyon tipi için seçimden düşer (bir
  satış kuralında olduğu gibi, 3.4); hiçbir oda hesaplanamazsa neden 422 olarak gelir.
- `/v1/prices`: odanın yalnızca bu pansiyon tipi düşer,
  `errorCode` ile `rooms[].errors[]` içinde bir kayıt olarak (3.2); geri kalanı fiyatlanmış kalır.
- `/v1/search`: otel düşer, kod `diagnostics.reasons` içinde yer alır.
- `priceCheck` ile `/v1/book`: 422, hiçbir şey rezerve edilmez.

---

## 4. Arama: `/v1/search` (BA)
<!-- de:710284c16578 -->

### 4.1 İstek ve yanıt
<!-- de:2e19c0fd56a1 -->

Tur operatörünün bir konaklama, bir pansiyon tipi ve bir konaklama düzeni için rezerve edilebilir
otellerini bulur, her otel için en uygun fiyatlı **müsait** odayla (`room` olmadan `/v1/price`
gibi): Dolu olan daha ucuz bir oda, başka bir oda rezerve edilebilir olduğu sürece
oteli listeden düşürmez.

| Alan | Zorunlu | Anlamı |
|---|---|---|
| `destination` | hayır | Otelin bölge veya havalimanı kodu (aşağıda); boş = tüm oteller |
| `board`, `checkIn`, `checkOut`, `occupancy`, `currency`, `now` | `/v1/price` gibi | |
| `includeUnavailable` | hayır | `true`: rezerve edilemeyen oteller işaretlenmiş olarak gelir (varsayılan `false`) |
| `pageSize`, `cursor` | hayır | Sayfalar, Bölüm 4.3 |

Yanıt:

| Alan | Anlamı |
|---|---|
| `results[]` | Sayfanın sonuçları, `fromTotalCents`'e göre artan, eşitlikte otel koduna göre |
| `results[].hotel`, `name`, `room` | Otel ve fiyatın ilişkili olduğu oda |
| `results[].fromTotalCents`, `currency` | Otelin sözleşme para biriminde başlangıç fiyatı (en uygun fiyatlı müsait oda; `bookable=false` durumunda genel olarak en uygun fiyatlı oda) |
| `results[].availability` | `/v1/price`'taki gibi |
| `results[].bookable` | `true` = tüm geceler müsait |
| `results[].reason`, `priceInformational` | yalnızca `bookable=false` durumunda (yalnızca `includeUnavailable` ile): neden ve „yalnızca fiyat bilgisi“ işareti |
| `diagnostics` | sayfa başına: `considered` = kontrol edilen oteller = `returned` + `skipped`; `reasons[]` = otellerin neden düştüğü, her neden için sayıyla; `timeBudgetExhausted` = sayfa zaman bütçesi nedeniyle kesildi (4.3); `roomErrors[]` = kontrol edilen otellerin bir sözleşme hatası (7.5) nedeniyle atlanan odaları, her kod için sayıyla – otel başka bir odayla eşleşse bile |
| `nextCursor` | kontrol edilecek başka oteller olduğu sürece dolu (4.3) |
| `warnings[]` | Uyarılar; karışık sözleşme para birimlerinde toplu bir uyarı |

Arama, düşen otellerin otel kodlarını belirtmez, yalnızca nedenleri ve sayıları. Boş
sonuç listesi bu nedenle „stok yok“ anlamına gelmez: `diagnostics.reasons` ör. sezonun
uymadığını (`ERR_NO_SECTION`), her şeyin dolu olduğunu (`ERR_NOT_AVAILABLE`), bir gece için
kontenjan tanımlanmadığını (`ERR_NO_INVENTORY`) veya otelin hiçbir odasının
istenen pansiyon tipini sunmadığını (`ERR_BOARD_NOT_OFFERED`) söyler.

**`destination` kodlarının nereden geldiği:** Her otelin, tur operatörünün sözleşmesinde
bir bölge kodu (ör. `PMI`) ve isteğe bağlı havalimanı kodları vardır; `destination`
bunlardan biriyle **tam olarak** aynıysa otel eşleşir (büyük/küçük harf önemlidir). Bölge kodları
yalnızca `A-Z a-z 0-9 . _ -` karakterlerinden oluşur (1 ile 64 karakter, serbest metin yok); havalimanı
kodları üç büyük harfli IATA kodlarıdır. Anahtarın geçerli kodlarını
`GET /v1/destinations` verir (4.5). Bu anahtar için hiç oteli olmayan bir kod
bir hatadır: `422 ERR_UNKNOWN_DESTINATION` (örnek `suche-unbekanntes-ziel`), asla
sessizce boş bir liste değil. İlk sayfa (`cursor` olmadan) kontrol edilir; hedef
sayfalama sırasında kaybolursa arama boş bir son sayfayla biter (4.3).

```http
### suche-ziel
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destination": "PMI",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-BASE", "name": "TEST Basis Palma", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
     "bookable": true},
    {"hotel": "TEST-HOTEL-DISCOUNT", "name": "TEST Rabatt Palma", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
     "bookable": true},
    {"hotel": "TEST-HOTEL-TWIN", "name": "TEST Zwilling A Palma", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
     "bookable": true}
  ],
  "diagnostics": {"considered": 3, "returned": 3, "skipped": 0}
}
```

Aynı kod küçük harfle yazıldığında API için farklı, bilinmeyen bir hedeftir:

```http
### suche-unbekanntes-ziel
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destination": "pmi",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{"errorCode": "ERR_UNKNOWN_DESTINATION",
 "message": "destination: kein Hotel mit diesem Ziel-Code fuer diesen Key (gueltige Codes: GET /v1/destinations; Gross-/Kleinschreibung zaehlt)"}
```

### 4.2 Rezerve edilemeyen oteller
<!-- de:c672dd1d20b9 -->

Varsayılan olarak arama yalnızca rezerve edilebilir sonuçları verir ve geri kalanını `diagnostics` içinde sayar.
`includeUnavailable=true` ile rezerve edilemeyen oteller `bookable=false`, `reason`
ve `priceInformational=true` ile gelir. Nedenler `/v1/book`'taki ile aynı anlama gelir:

- `ERR_NO_INVENTORY`: en az bir gece için hiç kontenjan tanımlı değildir – oda için
  hiç yoktur (`configured=false`) veya yalnızca bu gece için yoktur. `/v1/book`
  aynı konaklamayı `ERR_NO_INVENTORY` ile reddeder.
- `ERR_NOT_AVAILABLE`: her gecenin kontenjanı vardır, ancak her biri açık değildir (dolu,
  satış durdurma, kapalı, talep üzerine, müşteri grubunun tahsisi tükenmiş veya
  mevcut değil). Tam olarak hangi durum olduğunu `/v1/book` belirtir (`ERR_SOLD_OUT`, `ERR_STOP_SALE`,
  `ERR_INVENTORY_CLOSED`, `ERR_GROUP_LIMIT`, Bölüm 5.3).

Örnekte `TEST-HOTEL-SOLD`'un o gece için kapasitesi yoktur, `TEST-HOTEL-STOP`
satış durdurma durumundadır.

```http
### suche-nicht-buchbare
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destination": "IBZ",
  "includeUnavailable": true,
  "board": "RO",
  "checkIn": "{{D11}}",
  "checkOut": "{{D12}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-SOLD", "name": "TEST Ausgebucht Ibiza", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 10000, "availability": {"configured": true, "available": false, "minFree": 0},
     "bookable": false, "reason": "ERR_NO_INVENTORY", "priceInformational": true},
    {"hotel": "TEST-HOTEL-STOP", "name": "TEST Stop-Sale Ibiza", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 10000, "availability": {"configured": true, "available": false, "minFree": 0},
     "bookable": false, "reason": "ERR_NOT_AVAILABLE", "priceInformational": true}
  ],
  "diagnostics": {"considered": 2, "returned": 2, "skipped": 0}
}
```

### 4.3 Sayfalar ve cursor
<!-- de:3844e71067b2 -->

- Bir istek, otel kodu sırasıyla en fazla `pageSize` otelden oluşan **bir sayfayı** kontrol eder (varsayılan 50, en fazla
  100; 1–100 dışında: `422 ERR_BAD_PAGE_SIZE`).
- Başka oteller varsa yanıtta `nextCursor` yer alır. Sonraki sayfa: **aynı
  istek** artı `"cursor": "<nextCursor>"`. Son sayfada `nextCursor` yoktur.
- Fiyat sıralaması **sayfa başına** geçerlidir. Fiyata göre tam bir listeye ihtiyaç duyan
  sayfaları gezer ve kendisi sıralar. `diagnostics` sayfa başına geçerlidir.
- Cursor opaktır ve isteğe ve anahtara bağlıdır. Farklı istek (hedef,
  dönem, konaklama düzeni, pansiyon tipi, `includeUnavailable`) veya farklı anahtar: `422
  ERR_CURSOR_MISMATCH`. Okunamayan veya süresi dolmuş cursor (ör. sunucunun yeniden
  başlatılmasından sonra): `422 ERR_BAD_CURSOR` – o zaman `cursor` olmadan yeniden başlayın. `pageSize` ve
  `currency` sayfalar arasında değişebilir. Cursor'ın uzunluğu sabittir ve otel hakkında
  hiçbir şey söylemez.
- **Kesilmiş sayfa:** Yük altında bir sayfa 60 ms'den sonra yeni bir otele başlamaz. Bu durumda
  `pageSize`'dan kısadır, `diagnostics.timeBudgetExhausted=true` olur ve `nextCursor`
  devam ettirir. Bu bir hata değildir: sayfalamaya devam edin.
- Ne `pageSize` ne de `cursor` gönderilirse ve daha fazla otel varsa, ek olarak
  `warnings` içinde bir uyarı yer alır – liste o zaman yalnızca ilk sayfadır.

```http
### suche-seitenweise
POST {{baseUrl}}/v1/search
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "pageSize": 3,
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-ALTAKTION", "name": "TEST Altaktion Teneriffa", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
     "bookable": true},
    {"hotel": "TEST-HOTEL-BASE", "name": "TEST Basis Palma", "room": "DZ", "currency": "EUR",
     "fromTotalCents": 18000, "availability": {"configured": true, "available": true, "minFree": 5},
     "bookable": true}
  ],
  "diagnostics": {"considered": 3, "returned": 2, "skipped": 1,
                  "reasons": [{"reason": "ERR_NO_INVENTORY", "count": 1}]},
  "nextCursor": "{{*}}"
}
```

### 4.4 Aramanın yükü ve süre sınırları
<!-- de:0c0f3af46ae9 -->

Arama en maliyetli istektir. Tur operatörü başına aynı anda en fazla 8 arama çalışır; bunlar
hesaplama süresini paylaşır (tur operatörü başına sunucu çekirdeklerinin dörtte biri), böylece
yoğun bir tur operatörü diğerlerini yavaşlatmaz. Kota doluysa hemen
`Retry-After: 1` ile `429 ERR_SEARCH_BUSY` gelir. 10 sn'den uzun hesaplama yapan bir arama
`503 ERR_SEARCH_TIMEOUT` ile iptal edilir – asla sessizce kısaltılmış bir listeyle değil (yük altında
kesilmiş bir sayfada ise her zaman `nextCursor` vardır, 4.3). Çözüm: `destination` belirtin veya
daha küçük `pageSize`. Çağıran bağlantıyı kapatırsa arama hesaplamasını
iptal eder ve artık yanıt vermez.

### 4.5 `GET /v1/destinations` – geçerli hedef kodları
<!-- de:1a42c6ea9f71 -->

Bu anahtarın aramasında `destination`'ın filtreleyebileceği tüm kodları verir: tur operatörünün
otellerinin bölge ve havalimanı kodları, artan sırada, her kod bir kez. Yalnızca
kendi kodları – bir anahtar asla başka bir tur operatörünün hedeflerini görmez, bir
kontenjan grubunun anahtarı yalnızca tahsisi olan otellerinin hedeflerini (1.1; tahsis yoksa liste
boştur). Parametresizdir; liste, tur operatörü otel eklediğinde veya
kaldırdığında değişir – bir kontenjan grubunun anahtarında ayrıca tur operatörü onun tahsislerini
değiştirdiğinde ve bir müşteri grubunun her anahtarında tur operatörü grubun türünü değiştirdiğinde.

| Alan | Anlamı |
|---|---|
| `destinations[].code` | Kod, `destination` içine tam olarak böyle aktarılır |
| `destinations[].name` | Gösterim için açık metin; TourAPI kodu tanımıyorsa yoktur |
| `warnings[]` | Uyarılar, ör. gönderilen parametrelerle ilgili (yok sayılırlar) |

```http
### ziele
GET {{baseUrl}}/v1/destinations
X-Api-Key: {{apiKey}}
```

```json
{
  "destinations": [
    {"code": "ACE", "name": "Lanzarote"},
    {"code": "AGP", "name": "Malaga / Costa del Sol"},
    {"code": "ALC", "name": "Alicante / Costa Blanca"},
    {"code": "BCN", "name": "Barcelona"},
    {"code": "FAO", "name": "Faro / Algarve"},
    {"code": "FUE", "name": "Fuerteventura"},
    {"code": "IBZ", "name": "Ibiza"},
    {"code": "LPA", "name": "Gran Canaria"},
    {"code": "MAH", "name": "Menorca"},
    {"code": "PMI", "name": "Palma de Mallorca"},
    {"code": "RHO", "name": "Rhodos"},
    {"code": "TFS", "name": "Teneriffa Sued"}
  ]
}
```

---

## 4a. Açık arama: `POST /v1/search/open` (BA)
<!-- de:21abb0f088a4 -->

### 4a.1 Ne için
<!-- de:92d0c68259b8 -->

„1 ile 30 Ekim arasında 5 ila 7 gece için en ucuz nerede?“ – otel kodu olmadan ve sabit
tarih olmadan. Açık arama her otel için pencerenin tüm varış günleri, aralığın tüm
konaklama süreleri, tüm odalar ve pansiyon tipleri üzerinden **en iyi teklifi** verir ve
bunları tüm sayfalar boyunca **genel olarak sıralar**. İki taahhüt:

- **Fiyat kesindir:** `best.totalCents` ve `best.perTravellerCents`, aynı otel, oda,
  pansiyon tipi, varış ve ayrılış ve aynı konaklama düzeniyle `/v1/price` ile bit bit
  aynıdır. Bir önbellekten başlangıç fiyatları yok, örnekleme yok.
- **Sıralama kanıtlanmıştır:** gösterilmeyen hiçbir otelin en iyi teklifi son sonuçtan daha
  iyi değildir. Yük altında yalnızca sayfa kısalır (4a.4); fiyat asla daha az kesin olmaz,
  sıralama asla tahmin edilmez.

Bir web sitesinin akışı: açık arama (liste) → tarih matrisi ile bir otelin „tarihler ve
fiyatlar“ı (4b, aynı pencere) → seçilen teklifin `/v1/price` ile ayrıntısı (`best` veya
hücre içinden `hotel`, `room`, `board`, `checkIn`, `checkOut`, aynı konaklama düzeni:
dökümüyle birlikte aynı fiyat; bu tarihlerin diğer pansiyon tipleri, her pansiyon tipi için
`room` olmadan `/v1/price` ile) → `priceCheck.expectedCents` = gösterilen fiyat ile
`/v1/book`. Bir teklif token'ı yoktur: rezervasyon fiyatı zaten kontrol eder.

**Yetki:** Açık arama anahtar başına açılır (yeni anahtarlarda kapalı). Tur operatörü
yöneticisi anahtarda **arama profilini** belirler (pencere, süreler, hedefler, sayfa boyutu,
zaman bütçesi, istek hızı); en fazla işletmecinin tur operatörüne verdiği kadarı geçerlidir.
Yetki yoksa: `403
ERR_OPEN_SEARCH_NOT_ALLOWED`. API profilin sınırlarını alan bazında kontrol eder ve hata
mesajında belirtir (4a.5); `GET /v1/limits` bunları makine tarafından okunabilir biçimde verir (4c).

### 4a.2 İstek
<!-- de:2d7797b631a5 -->

| Alan | Zorunlu | Anlamı |
|---|---|---|
| `destinations` | hayır | `GET /v1/destinations`'daki gibi hedef kodları (4.5), birleşim; en fazla profilin izin verdiği kadar |
| `hotels` | hayır | Anahtarın otel kodları; `destinations` ile birlikte değil. İkisi de yoksa: anahtarın tüm stoku – yalnızca profil tüm hedeflere izin veriyorsa |
| `arrivalFrom`, `arrivalTo` | evet | Varış penceresi, her iki gün dahil; `arrivalFrom` ≥ referans tarihi, `arrivalTo` ≤ referans tarihi + 732 |
| `nightsMin`, `nightsMax` | evet | Konaklama süresi en az–en çok (1–30 ve profil içinde); aradaki her süre sayılır |
| `occupancy` | evet | tek oda, `/v1/price`'taki gibi |
| `boards` | hayır | Pansiyon tipi kodları, tam olarak sözleşmedeki gibi; yoksa = tümü |
| `boardTypes` | hayır | EDF dışa aktarımındaki gibi pansiyon türü (`AO`, `BB`, `HB`, `HB+`, `FB`, `FB+`, `SC`, `AI`, `AI+`, `XX`); `boards` ile birlikte: ikisi de uymalı |
| `minTotalCents`, `maxTotalCents` | hayır | Toplam fiyata filtre, sınırlar dahil |
| `currency` | koşullu | Sözleşme para birimine filtre, dönüştürme yok. Aramanın otelleri birden fazla para biriminde fiyatlandırıyorsa zorunlu (`422 ERR_CURRENCY_REQUIRED`) |
| `category` | hayır | resmi kategori (otel ana verilerindeki ulusal sınıflandırma): `scheme` `stars` veya `keys` (zorunlu), `min`/`max` 1 ile 5 arası yarım adımlarla sayı olarak seviye (örn. `3.5`, eşdeğer `3.50`), sınırlar dahil (yoksa: 1 veya 5). "4 Superior" 4 sayılır |
| `regions` | hayır | otel ana verilerindeki (adres) bölgeler, bunlardan biri; girildiği gibi tam karşılaştırılır (büyük/küçük harf önemlidir); en fazla 50 |
| `geo` | hayır | yarıçap: `lat`, `lon` (sayı olarak ondalık derece, en fazla 6 ondalık basamak) ve `radiusKm` (0,001 ile 500 arası, en fazla 3 ondalık basamak). Küre üzerinde mesafe (haversine, dünya yarıçapı 6371 km), tam metreye yuvarlanır; sınır içeride sayılır. Tarih çizgisi üzerinden ve kutuplarda doğrudur |
| `sort` | hayır | `price` (varsayılan: toplam fiyat), `pricePerNight` (gece başına fiyat, yuvarlamadan kesir olarak tam karşılaştırılır), `hotel` (otel kodu) |
| `pageSize` | hayır | Sayfa başına sonuç, 1'den profile kadar; belirtilmezse 20 (profil daha azına izin veriyorsa daha az) |
| `cursor` | hayır | Önceki sayfanın `nextCursor`'ı, değiştirilmeden (4a.4) |

Diğer sorgu endpoint'lerinin aksine açık arama **her** bilinmeyen alanı reddeder
(`422 ERR_UNKNOWN_FIELD`) – aksi halde bir filtredeki yazım hatası sonuç listesini sessizce
değiştirirdi. `now` yoktur; sunucunun referans tarihi geçerlidir (1.3).

**Otel ana verilerine göre filtreler** (`category`, `regions`, `geo`; birlikte VE geçerlidir)
hesaplamadan önce uygulanır: dışarıda kalan bir otel arama kapsamına ait değildir – ne
`hotelsInScope` içinde sayılır ne de arama profilinin istek başına otel sınırına
(`ERR_SEARCH_TOO_BROAD`) karşı; böylece tüm stok üzerinde bir yarıçap da mümkündür. Bir
otelde bir filtrenin ihtiyaç duyduğu değer eksikse (resmi kategori, bölge veya koordinat yok)
ve mevcut hiçbir değer onu dışlamıyorsa, arama kapsamında `ERR_NO_CATEGORY`, `ERR_NO_REGION`
veya `ERR_NO_GEO` nedeniyle sayılır (bu sıradaki ilk eksik değer) – otel ana verilerindeki
boşluklar görünür kalır. Değerleri tur operatörü otel ana verilerinde girer (konsol: otel →
İçerik → Ana veri ve konum); bir değişiklik bir sonraki eşitlemeden sonra (saniyeler) geçerli
olur ve `stand` değişir (4a.4).

### 4a.3 Yanıt
<!-- de:de059448ccbb -->

| Alan | Anlamı |
|---|---|
| `results[].hotel` | Otel kodu; `sort` ölçütüne göre sıralı, eşitlikte otel koduna göre |
| `results[].best` | en iyi teklif: `checkIn`, `checkOut`, `nights`, `room`, `board`, `boardType` (EDF dışa aktarımındaki gibi pansiyon türü), `currency`, `totalCents`, `perTravellerCents`, `availability` (`/v1/price`'taki gibi, her zaman `available: true`) |
| `results[].alternatives` | **Fiyatsız ön kontrol:** `dates` = müsaitlik, satış kuralları, sezon ve konaklama düzenine göre en az bir teklifi olan tarihler (varış × süre), `boards`/`boardTypes`/`rooms` buna göre. Bir üst sınırdır: fiyat hesaplaması bunlardan bazılarını yine de dışlayabilir. Asla fiyat veya sonuç ifadesi olarak okumayın |
| `coverage.complete` | `true`: sayfa dolu veya liste bitti. `false`: zaman bütçesi sayfayı kısalttı (4a.4) |
| `coverage.timeBudgetExhausted` | Sayfa zaman bütçesi nedeniyle kısaltıldı (= `complete: false`) |
| `coverage.standChanged` | veriler önceki sayfadan bu yana değişti (4a.4) |
| `coverage.hotelsInScope` | Arama kapsamındaki oteller (hedefler veya `hotels`, anahtarın stoku, otel ana verilerine göre filtreler; filtrelenen değeri olmayan oteller de sayılır, 4a.2) |
| `coverage.hotelsFeasible` | bunlardan ön kontrole göre en az bir tarihi olanlar – sonuç sayısının üst sınırı („N otele kadar“), sonuç sayısı değil |
| `coverage.hotelsPriced` | bu sayfada tam olarak hesaplanan oteller (sunucunun paralel olarak kaç otel hesapladığına da bağlıdır; bu yüzden örneklerde açık bırakılmıştır) |
| `coverage.undecided` | Zaman bütçesi bittiğinde yeri henüz belirsiz olan oteller (`complete` durumunda 0) |
| `coverage.reasons[]` | Tek bir tarihi bile olmayan oteller, neden başına – tüm arama kapsamı için, her sayfada aynı. Hiçbir oda uygun bir pansiyon tipi sunmuyorsa: `ERR_BOARD_NOT_OFFERED`. `/v1/search`'te olduğu gibi sonra müsaitlik gelir: hiçbir odanın tek bir boş tarihi bile yoksa neden odur (`ERR_NO_INVENTORY`, `ERR_NOT_AVAILABLE`). Aksi halde `/v1/price`'ın ilk boş tarih için (oda, pansiyon tipi, varış, süre bu sırayla) verdiği yanıt geçerlidir, ör. `ERR_OCCUPANCY_NOT_ALLOWED`, `ERR_NO_SECTION`, `ERR_STAY_LENGTH_NOT_ALLOWED` veya fiyatı filtrenin dışındaysa `ERR_OUTSIDE_PRICE_FILTER`; ayrıca `ERR_CURRENCY_NOT_AVAILABLE` (otel başka veya bilinmeyen bir para biriminde fiyatlandırıyor), `ERR_HOTEL_NOT_FOUND` (müşteri grubu için teklif yok) ve `ERR_NO_CATEGORY`, `ERR_NO_REGION`, `ERR_NO_GEO` (otelde otel ana verilerine göre bir filtrenin değeri eksik, 4a.2) |
| `coverage.priceReasons[]` | Ancak bu sayfadaki tam hesaplamada elenen oteller, neden başına (ör. `ERR_OUTSIDE_PRICE_FILTER`) |
| `coverage.roomErrors[]` | Sözleşme hatası (7.5) olan odalar, kod başına – asla sessizce atlanmaz |
| `stand` | Bu sayfanın veri durumunun tanımlayıcısı (opak) |
| `nextCursor` | başka sonuçlar gelebildiği sürece dolu |
| `warnings[]` | Uyarılar, ör. veri durumunun değişmesi hakkında |

**Bir otelin en iyi teklifi:** filtrenin tüm tarihler × odalar × pansiyon tipleri üzerinden
ölçütün (toplam fiyat veya gece başına fiyat) minimumu, yalnızca rezerve edilebilir
teklifler. Eşitlikte önce erken varış, sonra kısa süre, sonra sözleşmedeki sırayla oda ve
pansiyon tipi kazanır. `sort=hotel` ile ölçüt toplam fiyattır.

```http
### suche-offen
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destinations": ["PMI"],
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "pageSize": 2
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-BASE",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
              "boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}},
    {"hotel": "TEST-HOTEL-DISCOUNT",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
              "boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
               "hotelsInScope": 3, "hotelsFeasible": 3, "hotelsPriced": "{{*}}", "undecided": 0,
               "reasons": [], "priceReasons": [], "roomErrors": []},
  "stand": "{{*}}",
  "nextCursor": "{{*}}"
}
```

Sonraki sayfa, `cursor` eklenmiş aynı istektir:

```http
### suche-offen-weiter
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destinations": ["PMI"],
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "pageSize": 2,
  "cursor": "{{offenCursor}}"
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-TWIN",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
              "boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
               "hotelsInScope": 3, "hotelsFeasible": 3, "hotelsPriced": "{{*}}", "undecided": 0,
               "reasons": [], "priceReasons": [], "roomErrors": []},
  "stand": "{{*}}"
}
```

Tüm stok üzerinden, gece başına fiyata göre, yalnızca kahvaltı veya yarım pansiyon.
Pencerede kontenjanı olmayan oteller `coverage.reasons` içinde neden olarak yer alır:

```http
### suche-offen-je-nacht
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D13}}",
  "nightsMin": 3,
  "nightsMax": 5,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "boardTypes": ["BB", "HB"],
  "sort": "pricePerNight",
  "pageSize": 3
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-ALTAKTION",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
              "boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}},
    {"hotel": "TEST-HOTEL-BASE",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
              "boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
              "availability": {"configured": true, "available": true, "minFree": 4}},
     "alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}},
    {"hotel": "TEST-HOTEL-DISCOUNT",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D5}}", "nights": 5, "room": "DZ", "board": "BB",
              "boardType": "BB", "currency": "EUR", "totalCents": 62000, "perTravellerCents": [52000, 10000],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 42, "boards": ["BB", "HB"], "boardTypes": ["BB", "HB"], "rooms": 1}}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
               "hotelsInScope": 19, "hotelsFeasible": 13, "hotelsPriced": "{{*}}", "undecided": 0,
               "reasons": [{"reason": "ERR_NO_INVENTORY", "count": 6}], "priceReasons": [], "roomErrors": []},
  "stand": "{{*}}",
  "nextCursor": "{{*}}"
}
```

Palma ve Menorca genelinde otel ana verilerine göre filtrelerle: en az 3,5 yıldız, Mallorca
bölgesi, Palma çevresinde 25 km. Alcúdia'daki 3 yıldızlı otel dışarıda kalır, ana verileri
girilmemiş Menorca'daki iki otel neden olarak görünür:

```http
### suche-offen-stamm
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "destinations": ["PMI", "MAH"],
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "category": {"scheme": "stars", "min": 3.5},
  "regions": ["Mallorca"],
  "geo": {"lat": 39.57, "lon": 2.65, "radiusKm": 25}
}
```

```json
{
  "results": [
    {"hotel": "TEST-HOTEL-BASE",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
              "boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}},
    {"hotel": "TEST-HOTEL-DISCOUNT",
     "best": {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "room": "DZ", "board": "RO",
              "boardType": "AO", "currency": "EUR", "totalCents": 18000, "perTravellerCents": [18000, 0],
              "availability": {"configured": true, "available": true, "minFree": 5}},
     "alternatives": {"dates": 14, "boards": ["BB", "HB", "RO"], "boardTypes": ["AO", "BB", "HB"], "rooms": 1}}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "standChanged": false,
               "hotelsInScope": 4, "hotelsFeasible": 2, "hotelsPriced": "{{*}}", "undecided": 0,
               "reasons": [{"reason": "ERR_NO_CATEGORY", "count": 2}], "priceReasons": [], "roomErrors": []},
  "stand": "{{*}}"
}
```

### 4a.4 Sayfalar, cursor, veri durumu, zaman bütçesi
<!-- de:e3fd213995ec -->

- **Sayfalar:** `nextCursor` sonraki sayfaya götürür: **aynı istek** artı
  `"cursor": "<nextCursor>"`; `pageSize` değişebilir. Sonraki sayfa son sonucun
  arkasından devam eder (ölçüt, sonra otel kodu) – tüm sayfalar birlikte **tek bir**
  sıralı listedir. Son sayfada `nextCursor` yoktur; o sayfa boş da olabilir. `sort=price` ve
  `pricePerNight` ile sonraki sayfa, önceki sayfaların otellerini yeniden hesaplamaz: listenin
  derinlerinde de bir sayfa yaklaşık ilk sayfa kadar sürer.
- **Cursor:** opaktır, isteğe ve anahtara bağlıdır, **15 dakika** geçerlidir. Farklı
  istek (`pageSize` dışında herhangi bir alan) veya farklı anahtar: `422 ERR_CURSOR_MISMATCH`.
  Okunamayan, süresi dolmuş veya sabit cursor anahtarı olmadan sunucu yeniden başlatıldıktan
  sonra: `422
  ERR_BAD_CURSOR` – o zaman `cursor` olmadan yeniden başlayın. `/v1/search`'ten gelen bir
  cursor burada geçerli değildir.
- **Veri durumu:** `stand`, tur operatörü sözleşmeleri, promosyonları veya tahsisleri
  yayınladığında ya da otel ana verilerinde bir otelin bölgesini, koordinatlarını veya resmi
  kategorisini değiştirdiğinde ve gece yarısı (referans tarihi) değişir; rezervasyonlarda veya
  diğer içeriklerde (metinler, görseller) değişmez.
  Veri durumu önceki sayfadan bu yana değiştiyse sonraki sayfa aynı yerden **yeni** durumda
  hesaplamaya devam eder; `coverage.standChanged: true` ve `warnings` içinde bir uyarıyla.
  O zaman (iki sayfa arasındaki başkalarının rezervasyonlarında da olduğu gibi) bir otel iki
  kez görünebilir veya eksik olabilir: `hotel`'e göre tekilleştirin. Her sayfanın fiyatları
  kendi veri durumu için geçerlidir; rezervasyonda fiyatı `priceCheck` güvenceye alır.
- **Zaman bütçesi:** Her sayfanın bir zaman bütçesi vardır (arama profili, ör. 150 ms),
  isteğin gelişinden itibaren sayılır. Bütçe tükendiğinde ve sayfada zaten bir sonuç varsa
  başka otel hesaplanmaz; yeri zaten kesinleşmiş sonuçlar yine de eklenir. O zaman:
  `coverage.complete: false`, `timeBudgetExhausted: true`, `undecided` > 0 ve kalanı için
  `nextCursor`. Gösterilen sonuçlar yine de kesin ve kanıtlanmış sıradadır – sayfalamaya
  devam edin. Bir sayfa, hâlâ bir sonuç varken asla sonuçsuz bitmez.
  Kısaltılmış sayfalar bir hata değildir. Özellikle çok büyük aramalarda (en büyük hedefte
  geniş konaklama süresi aralığına sahip uzun bir varış penceresi), aynı tur operatörünün
  birden fazla araması aynı anda çalışırken ortaya çıkar. Tam sayfalara ihtiyacınız varsa
  daha küçük pencereler veya aralıklar sorgulayın ya da aramaları paralel değil art arda
  yapın.
- **Yük:** Açık arama, tur operatörünün arama yerlerini ve hesaplama süresini `/v1/search`
  ile paylaşır (4.4), ancak arama yerlerinin en fazla yarısını kullanır (8'de 4; tur
  operatörünün tüm anahtarları ve iki uç nokta birlikte) – geri kalanı `/v1/search` için boş
  kalır. Ayrıca anahtar başına arama profilinin istek hızı ve eşzamanlı aramaları geçerlidir
  (`429 ERR_RATE_LIMITED` veya `ERR_SEARCH_BUSY`, `Retry-After` ile); `429 ERR_SEARCH_BUSY`
  ile reddedilen bir arama istek hızından düşmez. Bir arama 10 sn'den uzun hesaplarsa
  `503 ERR_SEARCH_TIMEOUT` ile biter, asla kısmi bir sayfayla değil.
- **Kanıtlanamıyor:** Arama bir istek için fiyatı, sıralamayı veya müsaitliği kanıtlayamazsa
  (isteğin değil, sunucunun bir hatası), muhtemelen yanlış bir sayfa yerine `500 ERR_INTERNAL`
  ile yanıt verir.

### 4a.5 Açık aramanın hataları
<!-- de:b477cf77b3ae -->

Kontroller bu sırayla çalışır; ilk bulgu bildirilir: anahtar → yetki → body ve bilinmeyen
alanlar → alan biçimi → API sınırları (1.3) → arama profilinin sınırları → hedefler,
oteller, pansiyon tipi, para birimi → cursor → istek hızı ve arama yerleri → hesaplama.
Reddedilen bir istek arama yeri işgal etmez ve arama olarak sayılmaz. Kodlar hata
kataloğundadır (7.2, 7.3).

```http
### suche-offen-ohne-recht
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{poolKey}}

{
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{"errorCode": "ERR_OPEN_SEARCH_NOT_ALLOWED",
 "message": "dieser API-Key hat kein Recht fuer die offene Suche — der Veranstalter-Admin schaltet es frei (Suchprofil des Keys)"}
```

```http
### suche-offen-fenster-zu-breit
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D30}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{"errorCode": "ERR_WINDOW_TOO_WIDE", "message": "arrivalFrom..arrivalTo: 31 Tage, erlaubt hoechstens 14 (Suchprofil des Keys)"}
```

Müşteri grubunun anahtarı yalnızca bir hedefte arama yapabilir:

```http
### suche-offen-ziel-nicht-erlaubt
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{partnerKey}}

{
  "destinations": ["PMI"],
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

```json
{"errorCode": "ERR_DESTINATION_NOT_ALLOWED", "message": "destinations[0]: Ziel ausserhalb der erlaubten Ziele dieses Keys (Suchprofil des Keys)"}
```

Yanlış değerli bir filtre alanı belirtir:

```http
### suche-offen-filter-kaputt
POST {{baseUrl}}/v1/search/open
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D6}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "geo": {"lat": 39.57, "lon": 2.65, "radiusKm": 600}
}
```

```json
{"errorCode": "ERR_BAD_FILTER", "message": "geo.radiusKm: 600 ausserhalb 0.001..500"}
```

---

## 4b. Tarih matrisi: `POST /v1/search/open/dates` (BA)
<!-- de:1ecb6c006f14 -->

### 4b.1 Ne için
<!-- de:99c6f48dd1bc -->

Bir otelin „tarihler ve fiyatlar“ı: **tek** bir otel ve açık aramayla aynı pencere için her
tarihte (varış × süre) rezerve edilebilir en ucuz teklif ya da neden teklif olmadığı. Yetki ve
sınırlar açık aramadaki aynı arama profilinden gelir (4a.1); profilin istek hızı ve eşzamanlı
aramaları iki endpoint için birlikte geçerlidir.

- **Fiyat kesindir:** teklif içeren her hücre, aynı otel, oda, pansiyon tipi, giriş ve çıkış
  ve aynı konaklama düzeniyle `/v1/price` ile bit düzeyinde aynıdır.
- **Açık aramayla uyumludur:** aynı filtre ve aynı `stand` ile 4a'daki `best`, en küçük
  ölçüte (toplam fiyat veya gecelik fiyat) sahip ilk hücredir – aynı eşitlik kuralı (daha
  erken varış, daha kısa süre, sözleşme sırasına göre oda ve pansiyon tipi). `perBoard` ile
  `best`, en küçük ölçüte sahip ilk tarihte kendi pansiyon tipinin hücresidir.

### 4b.2 İstek
<!-- de:69bed1913110 -->

| Alan | Zorunlu | Anlamı |
|---|---|---|
| `hotel` | evet | Anahtarın otel kodu (ör. 4a'dan `results[].hotel`) |
| `arrivalFrom`, `arrivalTo` | evet | 4a.2'deki gibi varış penceresi; en fazla `matrixMaxWindowDays` gün (arama profili) |
| `nightsMin`, `nightsMax` | evet | 4a.2'deki gibi süre aralığı (profilin izin verdiği süreler) |
| `occupancy` | evet | bir oda, `/v1/price` gibi |
| `boards`, `boardTypes` | hayır | 4a.2'deki gibi; `boards` içindeki her kod otelde sunulmalıdır (`422 ERR_BOARD_NOT_OFFERED`) |
| `rooms` | hayır | Otelin oda kodları; yoksa tümü (bilinmeyen: `404 ERR_ROOM_NOT_FOUND`) |
| `minTotalCents`, `maxTotalCents` | hayır | 4a.2'deki gibi toplam fiyat filtresi |
| `currency` | hayır | Otelin sözleşme para birimi; otel başka bir para biriminde veya geçerli bir para birimi olmadan fiyatlandırıyorsa: `422 ERR_CURRENCY_NOT_AVAILABLE` |
| `perBoard` | hayır | `true`: her tarih için **pansiyon tipi başına** bir hücre (varsayılan `false`: tarih başına bir hücre) |
| `category`, `regions`, `geo` | hayır | 4a.2'deki gibi otel ana verilerine göre filtreler, aynı etki: otel dışarıda kalırsa arama alanına ait değildir ve matriste **hiç hücre yoktur** (`cells` boş); değer eksikse her hücre `ERR_NO_CATEGORY`, `ERR_NO_REGION` veya `ERR_NO_GEO` ile `none` olur |

Hücreler = varış günleri × süreler (`perBoard` ile × pansiyon tipleri), en fazla
`matrixMaxCells` (arama profili, aksi halde `422 ERR_SEARCH_TOO_BROAD`). Cursor yoktur. Her
bilinmeyen alan: `422 ERR_UNKNOWN_FIELD`. Kontrol sırası açık aramanınkidir (4a.5), cursor olmadan.

### 4b.3 Yanıt
<!-- de:a7def6016c34 -->

| Alan | Anlamı |
|---|---|
| `hotel`, `currency` | Otel ve sözleşme para birimi |
| `stand` | 4a'daki gibi veri durumu (aynı `stand` = aynı veri) |
| `boards` | yalnızca `perBoard` ile: tarih başına pansiyon tipleri sözleşme sırasıyla (odalar üzerinden ilk geçiş, önce RO, `/v1/prices` gibi), hücrelerin sırasıyla |
| `cells[]` | `checkIn`, `nights` sırasıyla **boşluksuz** (`perBoard` ile ardından pansiyon tipi); her hücrede `checkIn`, `checkOut`, `nights`, `status`. Yalnızca otel, otel ana verilerine göre filtrelerin dışında kalırsa boştur |
| `cells[].status = "offer"` | Teklif: `room`, `board`, `boardType`, `totalCents`, `perTravellerCents`, `availability` (`/v1/price` gibi, her zaman `available: true`) – tarihin en ucuz odası/pansiyon tipi, eşitlikte sözleşme sırasına göre |
| `cells[].status = "none"` | teklif yok, neden `reason` içinde: kodlar `/v1/price` ile aynı, sıra açık aramadaki gibi (4a) – önce müsaitlik (`ERR_NO_INVENTORY`, `ERR_NOT_AVAILABLE`), sonra satış kuralları, konaklama düzeni, sezon, fiyat filtresi (`ERR_OUTSIDE_PRICE_FILTER`) veya bir sözleşme hatası; otel ana verilerine göre filtrelerle `ERR_NO_CATEGORY`, `ERR_NO_REGION`, `ERR_NO_GEO` (otelde değer eksik, her hücre) |
| `cells[].status = "unchecked"` | zaman bütçesi dolduğu için kontrol edilmedi – **asla** „teklif yok“ olarak okunmamalı |
| `coverage` | `complete` (hiçbir hücre `unchecked` değil), `timeBudgetExhausted`, `cells` = `offers` + `none` + `unchecked`, `roomErrors[]` (kod başına sözleşme hatalı odalar, 7.5) |
| `warnings[]` | Uyarılar, ör. kontrol edilmemiş hücreler hakkında |

Hücreler matris sırasıyla bloklar halinde hesaplanır; profilin zaman bütçesi dolduğunda kalan
hücreler `unchecked` kalır (ilk blok her zaman kontrol edilir). O zaman pencereyi veya
süreleri daraltıp yeniden sorun. Matris 10 sn'den uzun hesaplarsa:
`503 ERR_SEARCH_TIMEOUT`; kanıtlanamazsa: `500 ERR_INTERNAL` (4a.4'teki gibi).

```http
### suche-termine
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D1}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "maxTotalCents": 20000
}
```

```json
{
  "hotel": "TEST-HOTEL-BASE",
  "currency": "EUR",
  "stand": "{{*}}",
  "cells": [
    {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "RO",
     "boardType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
     "availability": {"configured": true, "available": true, "minFree": 5}},
    {"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "none", "reason": "ERR_OUTSIDE_PRICE_FILTER"},
    {"checkIn": "{{D1}}", "checkOut": "{{D3}}", "nights": 2, "status": "offer", "room": "DZ", "board": "RO",
     "boardType": "AO", "totalCents": 18000, "perTravellerCents": [18000, 0],
     "availability": {"configured": true, "available": true, "minFree": 5}},
    {"checkIn": "{{D1}}", "checkOut": "{{D4}}", "nights": 3, "status": "none", "reason": "ERR_OUTSIDE_PRICE_FILTER"}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "cells": 4, "offers": 2, "none": 2, "unchecked": 0,
               "roomErrors": []}
}
```

Pansiyon tipi başına, yalnızca kahvaltı ve yarım pansiyon:

```http
### suche-termine-je-verpflegung
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D0}}",
  "nightsMin": 2,
  "nightsMax": 3,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "boards": ["BB", "HB"],
  "perBoard": true
}
```

```json
{
  "hotel": "TEST-HOTEL-BASE",
  "currency": "EUR",
  "stand": "{{*}}",
  "boards": ["BB", "HB"],
  "cells": [
    {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "BB",
     "boardType": "BB", "totalCents": 26000, "perTravellerCents": [22000, 4000],
     "availability": {"configured": true, "available": true, "minFree": 5}},
    {"checkIn": "{{D0}}", "checkOut": "{{D2}}", "nights": 2, "status": "offer", "room": "DZ", "board": "HB",
     "boardType": "HB", "totalCents": 32000, "perTravellerCents": [25000, 7000],
     "availability": {"configured": true, "available": true, "minFree": 5}},
    {"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "offer", "room": "DZ", "board": "BB",
     "boardType": "BB", "totalCents": 38000, "perTravellerCents": [32000, 6000],
     "availability": {"configured": true, "available": true, "minFree": 5}},
    {"checkIn": "{{D0}}", "checkOut": "{{D3}}", "nights": 3, "status": "offer", "room": "DZ", "board": "HB",
     "boardType": "HB", "totalCents": 47000, "perTravellerCents": [36500, 10500],
     "availability": {"configured": true, "available": true, "minFree": 5}}
  ],
  "coverage": {"complete": true, "timeBudgetExhausted": false, "cells": 4, "offers": 4, "none": 0, "unchecked": 0,
               "roomErrors": []}
}
```

31 varış günü × 14 süre × 3 pansiyon tipi, profilin izin verdiğinden fazla hücredir:

```http
### suche-termine-zu-viele-zellen
POST {{baseUrl}}/v1/search/open/dates
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "arrivalFrom": "{{D0}}",
  "arrivalTo": "{{D30}}",
  "nightsMin": 1,
  "nightsMax": 14,
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
  "perBoard": true
}
```

```json
{"errorCode": "ERR_SEARCH_TOO_BROAD",
 "message": "Termin-Matrix: 1302 Zellen (Anreisen x Dauern x 3 Verpflegungen), erlaubt hoechstens 500 (Suchprofil des Keys)"}
```

---

## 4c. Anahtarın sınırları: `GET /v1/limits`
<!-- de:51a6dfcc8750 -->

**Kendi** anahtarının geçerli sınırları; böylece bir web sitesi sınırları `422` ile
yoklamak yerine tarih seçimini, süre seçimini ve hedefleri ayarlar. Parametre yok (herhangi
biri: `422
ERR_UNKNOWN_FIELD`), arama olarak sayılmaz.

| Alan | Anlamı |
|---|---|
| `rate` | Anahtarın tüm isteklerinin hızı (4.4): `perSecond`, `burst`, `scope` (`key` = anahtar ve düğüm başına; `testCircle` = test anahtarlarının ortak havuzu, 11) |
| `export.allowed` | EDF teslimatı yetkisi (10) |
| `content.allowed` | İçerik API'sine (`/v1/content/*`) erişim: işletmeci tur operatörü için içerikleri açmış **ve** anahtar içerik yetkisine sahip – oradakiyle aynı kural; erişim yoksa `403 ERR_CONTENT_NOT_ALLOWED` |
| `openSearch.allowed` | Açık arama ve tarih matrisi yetkisi; yetki yoksa yalnızca bu alan bulunur |
| `openSearch.maxWindowDays`, `nightsMin`, `nightsMax`, `maxNightsSpan` | Varış penceresi, izin verilen süreler ve istek başına süre aralığı (4a) |
| `openSearch.maxDestinations`, `maxHotels`, `maxCandidates`, `maxPageSize` | İstek başına hedefler ve oteller, arama kapsamındaki oteller, sayfa başına sonuçlar (4a) |
| `openSearch.timeBudgetMs`, `rate`, `burst`, `concurrency` | Sayfa veya matris başına zaman bütçesi, istek hızı ve eşzamanlı aramalar (iki endpoint birlikte) |
| `openSearch.matrixMaxWindowDays`, `matrixMaxCells` | Tarih matrisinin penceresi ve hücreleri (4b) |
| `openSearch.allDestinations`, `destinations` | tüm hedeflere izin var, aksi halde izin verilen hedef kodları – yalnızca anahtarın envanterinde oteli olanlar (`GET /v1/destinations` gibi) |

Değerler tam olarak `/v1/search/open` ve `/v1/search/open/dates` uç noktalarının uyguladığı
değerlerdir: işletmecinin tur operatörüne verdiği ile anahtarın profilinin en küçüğü. Bir
sınırı hangi seviyenin belirlediği yanıtta yer almaz. Profildeki değişiklikler birkaç
saniye sonra etkili olur.

```http
### grenzen
GET {{baseUrl}}/v1/limits
X-Api-Key: {{apiKey}}
```

```json
{
  "rate": {"perSecond": 200, "burst": 400, "scope": "key"},
  "export": {"allowed": true},
  "content": {"allowed": true},
  "openSearch": {"allowed": true, "maxWindowDays": 14, "nightsMin": 1, "nightsMax": 14, "maxNightsSpan": 3,
                 "maxDestinations": 3, "maxHotels": 20, "maxCandidates": 500, "maxPageSize": 20,
                 "timeBudgetMs": 150, "rate": 5, "burst": 20, "concurrency": 1,
                 "matrixMaxWindowDays": 31, "matrixMaxCells": 500, "allDestinations": true}
}
```

Açık arama yetkisi olmayan bir anahtar:

```http
### grenzen-ohne-recht
GET {{baseUrl}}/v1/limits
X-Api-Key: {{poolKey}}
```

```json
{
  "rate": {"perSecond": 200, "burst": 400, "scope": "key"},
  "export": {"allowed": false},
  "content": {"allowed": false},
  "openSearch": {"allowed": false}
}
```

---


## 5. Rezervasyon (B) ve rezervasyon bilgisi
<!-- de:eca640daa2c5 -->

### 5.1 `POST /v1/book`
<!-- de:d7902ce86d55 -->

| Alan | Zorunlu | Anlamı |
|---|---|---|
| `hotel`, `room` | evet | Otel ve oda kodu (rezervasyon pansiyon tipinden bağımsızdır; pansiyon tipi `priceCheck` içinde yer alır). `room` eksikse: `400 ERR_INVALID_BUCKET`, `priceCheck` ile ve olmadan |
| `checkIn`, `checkOut` | evet | Konaklama (Bölüm 1.3) |
| `quantity` | evet | Oda sayısı, 1–1.000.000 (`400 ERR_QUANTITY_INVALID`) |
| `idemKey` | evet | işlemin kendi benzersiz anahtarı (Bölüm 9), en fazla 128 karakter; eksikse: `400 ERR_INVALID_IDEM_KEY` |
| `reference` | hayır | kendi rezervasyon referansı, en fazla 128 karakter; rezervasyon daha sonra bununla okunabilir |
| `leadPaxName` | hayır | Ana yolcunun adı, en fazla 255 karakter |
| `metadata` | hayır | serbest JSON nesnesi, saklanır ve `/v1/booking`'de geri döndürülür, değerlendirilmez. `metadata.correlationId` (64 karaktere kadar) korelasyon kimliği olarak devralınır. |
| `priceCheck` | hayır, **önerilir** | Satıştan önce fiyat kontrolü, aşağıya bakın |

Çok uzun metinleri (`idemKey`, `reference`, `leadPaxName`, `metadata.correlationId`) API
her satıştan önce `422 ERR_VALIDATION` ile reddeder; `message` alanı, uzunluğu ve sınırı belirtir. Asla
kısaltılmaz.

`priceCheck`: `board`, `occupancy.travellers[]`, `expectedCents` (müşterinin gördüğü
fiyat), `tolerancePercent` (% olarak izin verilen sapma, ≥ 0), isteğe bağlı `currency` ve
`now`. TourAPI fiyatı `/v1/price` gibi yeniden hesaplar; tolerans değerinden fazla saparsa:
`message` içinde güncel fiyatla `409 ERR_PRICE_DRIFT`, **hiçbir şey rezerve edilmez**. `priceCheck`
ile TourAPI kontrol edilen fiyatı rezervasyonda saklar (`/v1/booking` içinde `totalCents`, `currency`,
`board`). `priceCheck` olmadan rezervasyon, fiyatsız saf bir kontenjan satışıdır
(`totalCents: null`). Bu durumda da `/v1/book` yalnızca `/v1/price`'ın bu
anahtar için tanıdığı otelleri satar: Geçerli sözleşme olmadan otel yoktur (`404 ERR_HOTEL_NOT_FOUND`),
kontenjan tanımlı olsa bile.

Yanıt: `booked`, `alreadyBooked` (`true` = tekrar, yeni bir şey satılmadı), `reference`
(bizim rezervasyon referansımız `TA-…`), `correlationId`. `correlationId`, gönderilen
`metadata.correlationId`'dir; yoksa TourAPI bir UUID atar ve bunu kayıtta saklar.
Bir tekrar (`alreadyBooked=true`) her zaman ilk çağrının saklanan `correlationId` değerini
belirtir – tekrar hiç göndermese veya farklı bir tane gönderse bile.

```http
### buchen
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0001",
  "reference": "KUNDE-4711",
  "leadPaxName": "Erika Muster",
  "metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
  "priceCheck": {
    "board": "RO",
    "currency": "EUR",
    "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
    "expectedCents": 18000,
    "tolerancePercent": 0
  }
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{buchungsRef}}", "correlationId": "kette-0815"}
```

### 5.2 Tekrarlamak güvenlidir
<!-- de:81e75ea42a1c -->

Aynı isteği aynı `idemKey` ile bir kez daha göndermek – örneğin bir ağ hatasından sonra – yeniden
satış yapmaz, mevcut rezervasyonu onaylar (`alreadyBooked: true`, aynı
referans).

```http
### buchen-wiederholt
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0001",
  "reference": "KUNDE-4711",
  "leadPaxName": "Erika Muster",
  "metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
  "priceCheck": {
    "board": "RO",
    "currency": "EUR",
    "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
    "expectedCents": 18000,
    "tolerancePercent": 0
  }
}
```

```json
{"booked": true, "alreadyBooked": true, "reference": "{{buchungsRef}}", "correlationId": "kette-0815"}
```

Farklı rezervasyon verileriyle (otel, oda, konaklama, miktar) aynı `idemKey`
çağıranın hatasıdır:

```http
### buchen-anderer-vorgang
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 2,
  "idemKey": "BEISPIEL-0001"
}
```

```json
{"errorCode": "ERR_IDEMPOTENCY_MISMATCH", "message": "idemKey bereits mit anderen Buchungsdaten (hotel, room, checkIn, checkOut, quantity) vergeben"}
```

### 5.3 Bir rezervasyon neden başarısız olur
<!-- de:b1c81ac323ca -->

Satış bir gecede başarısız olursa `message` etkilenen ilk geceyi belirtir. Hiçbir şey
rezerve edilmez (ya tüm geceler ya hiçbiri).

| Kod | Durum | Anlamı |
|---|---|---|
| `ERR_SOLD_OUT` | 422 | Gece dolu |
| `ERR_STOP_SALE` | 422 | Tur operatörü satışı durdurmuş |
| `ERR_INVENTORY_CLOSED` | 422 | Gece kapalı veya yalnızca talep üzerine |
| `ERR_NO_INVENTORY` | 422 | bir gece için kapasite tanımlı değil |
| `ERR_GROUP_LIMIT` | 422 | müşteri grubunun tahsisi tükenmiş (veya oda ve gece için mevcut değil) |
| `ERR_HOTEL_NOT_FOUND` | 404 | Otel bu anahtar için yok (ayrıca: geçerli sözleşme yok, `priceCheck` ile ve olmadan; teklifi olmayan müşteri grubu, 1.1) |
| `ERR_PRICE_DRIFT` | 409 | Fiyat `priceCheck`'ten sapıyor |
| `ERR_STAY_LENGTH_NOT_ALLOWED`, `ERR_ARRIVAL_DAY_NOT_ALLOWED`, `ERR_TRAVEL_DATES_NOT_ALLOWED`, `ERR_BOARD_NOT_ALLOWED`, `ERR_LEAD_TIME_NOT_ALLOWED` | 422 | odanın bir satış kuralı konaklamayı hariç tutuyor (3.4); hiçbir şey rezerve edilmez |
| `ERR_BOARD_NOT_AVAILABLE` | 422 | `priceCheck`'teki pansiyon tipi bu yolcu grubuna satılmıyor (3.5); hiçbir şey rezerve edilmez |

```http
### buchen-preis-geaendert
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0002",
  "priceCheck": {
    "board": "RO",
    "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
    "expectedCents": 17000,
    "tolerancePercent": 2
  }
}
```

```json
{"errorCode": "ERR_PRICE_DRIFT", "message": "Preis hat sich geaendert: aktuell 18000 Cent, erwartet 17000 Cent"}
```

```http
### buchen-ausgebucht
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-SOLD",
  "room": "DZ",
  "checkIn": "{{D13}}",
  "checkOut": "{{D14}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0003"
}
```

```json
{"errorCode": "ERR_SOLD_OUT", "message": "Nacht {{D13}} ausgebucht"}
```

```http
### buchen-stop-sale
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-STOP",
  "room": "DZ",
  "checkIn": "{{D11}}",
  "checkOut": "{{D12}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0004"
}
```

```json
{"errorCode": "ERR_STOP_SALE", "message": "Nacht {{D11}} stop-sale"}
```

Tahsisli müşteri grubuna sahip bir anahtar, serbest satıştaki gecelerde de kendi grubunun
tahsisine karşı rezervasyon yapar. Burada `TEST-PARTNER-BASE`'e o gece için 5 tahsis edilmiştir:

```http
### buchen-zuteilung-erschoepft
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{partnerKey}}

{
  "hotel": "TEST-HOTEL-FREESALE",
  "room": "DZ",
  "checkIn": "{{D20}}",
  "checkOut": "{{D21}}",
  "quantity": 6,
  "idemKey": "BEISPIEL-0005"
}
```

```json
{"errorCode": "ERR_GROUP_LIMIT", "message": "Gruppen-Kontingent fuer {{D20}} erschoepft"}
```

Bir fiyat grubu, grubu olmayan bir anahtar gibi genel stoktan rezervasyon yapar:

```http
### buchen-preisgruppe
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{rabattKey}}

{
  "hotel": "TEST-HOTEL-DISCOUNT",
  "room": "DZ",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0006"
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{*}}", "correlationId": "{{*}}"}
```

### 5.4 `GET /v1/booking?ref=…` – rezervasyon bilgisi
<!-- de:61342849d606 -->

`ref` bizim referansımız (`TA-…`) **veya** rezervasyondaki kendi `reference` değerinizdir; bizim
referansımız önceliklidir. Kendi `reference` değeriniz birden fazla görünür rezervasyonla eşleşirse (iptal edilmişler
dahil): `409 ERR_REFERENCE_AMBIGUOUS`, `message` eşleşen `TA-…` referanslarını belirtir
(önce en yeni, en fazla 10) – o zaman rezervasyon yanıtındaki bizim referansımızla okuyun. Yanıt:
`reference`, `customerReference`, `hotel`, `room`, `group` (yalnızca müşteri grubu rezervasyonlarında; grup
kodu `A-Z a-z 0-9 . _ -` karakterlerinden, 1 ile 64 karakter),
`checkIn`, `checkOut`, `quantity`, `status` (`confirmed` | `released`), `bookedAt`,
`updatedAt` (son değişiklik, iptalde iptal zamanı; ikisi de UTC olarak RFC 3339, ör.
`2026-09-26T08:15:03Z`), `metadata`, `totalCents`, `currency`, `board`. Yolcuların kişisel
verilerini API döndürmez.

| Rezervasyon … | rezervasyon bilgisinde |
|---|---|
| `priceCheck` ile | `totalCents` = kontrol edilen fiyat, `currency` = sözleşme para birimi, `board` = `priceCheck`'teki pansiyon tipi |
| `priceCheck` olmadan | `totalCents: null`, `currency: ""`, `board: ""` (boş metinler, fiyat kaydedilmedi) |
| `reference` olmadan | `customerReference` yok |
| `metadata` olmadan | `metadata` yok |
| müşteri grubu olmayan anahtarla | `group` yok |

Görünürlük: Müşteri grubuna sahip bir anahtar yalnızca kendi grubunun rezervasyonlarını görür. Temel
sözleşmedeki bir anahtar tur operatörünün tüm rezervasyonlarını görür, müşteri gruplarınınkiler dahil – ancak
yalnızca kendi rezervasyonlarını iptal edebilir (Bölüm 6). Bilinmeyen veya görünür değil:
`404 ERR_BOOKING_NOT_FOUND`.

```http
### buchung-lesen
GET {{baseUrl}}/v1/booking?ref={{buchungsRef}}
X-Api-Key: {{apiKey}}
```

```json
{
  "reference": "{{buchungsRef}}",
  "customerReference": "KUNDE-4711",
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "status": "confirmed",
  "bookedAt": "{{*}}",
  "updatedAt": "{{*}}",
  "metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
  "totalCents": 18000,
  "currency": "EUR",
  "board": "RO"
}
```

Kendi referansınız üzerinden:

```http
### buchung-lesen-kundenreferenz
GET {{baseUrl}}/v1/booking?ref=KUNDE-4711
X-Api-Key: {{apiKey}}
```

```json
{
  "reference": "{{buchungsRef}}",
  "customerReference": "KUNDE-4711",
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "status": "confirmed",
  "bookedAt": "{{*}}",
  "updatedAt": "{{*}}",
  "metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
  "totalCents": 18000,
  "currency": "EUR",
  "board": "RO"
}
```

`priceCheck`, `reference` ve `metadata` olmayan bir rezervasyon – yanıt sunucu tarafından
atanmış bir `correlationId` taşır, rezervasyon bilgisinde fiyat yoktur:

```http
### buchen-ohne-preispruefung
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D20}}",
  "checkOut": "{{D21}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0010"
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{ohnePreisRef}}", "correlationId": "{{*}}"}
```

```http
### buchung-lesen-ohne-preispruefung
GET {{baseUrl}}/v1/booking?ref={{ohnePreisRef}}
X-Api-Key: {{apiKey}}
```

```json
{
  "reference": "{{ohnePreisRef}}",
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D20}}",
  "checkOut": "{{D21}}",
  "quantity": 1,
  "status": "confirmed",
  "bookedAt": "{{*}}",
  "updatedAt": "{{*}}",
  "totalCents": null,
  "currency": "",
  "board": ""
}
```

Aynı kendi referansının ikinci bir rezervasyonda kullanılması onu belirsiz yapar:

```http
### buchen-gleiche-kundenreferenz
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D22}}",
  "checkOut": "{{D23}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0011",
  "reference": "KUNDE-4711"
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{zweiteRef}}", "correlationId": "{{*}}"}
```

```http
### buchung-lesen-mehrdeutig
GET {{baseUrl}}/v1/booking?ref=KUNDE-4711
X-Api-Key: {{apiKey}}
```

```json
{"errorCode": "ERR_REFERENCE_AMBIGUOUS", "message": "ref 'KUNDE-4711' passt zu mehreren Buchungen ({{zweiteRef}}, {{buchungsRef}}) — mit der TourAPI-Referenz (TA-…) lesen"}
```

---

## 6. İptal (S): `POST /v1/cancel`
<!-- de:6345c44aa08c -->

Tek alan: rezervasyonun `idemKey` değeri (eksikse: `400 ERR_INVALID_IDEM_KEY`; başka her
alan: `422 ERR_UNKNOWN_FIELD`). Tüm gecelerin kontenjanı iade edilir,
durum `released` olur. Yalnızca **aynı anahtar çevresiyle** rezerve edilmiş olan iptal
edilebilir: Müşteri grubuna sahip bir anahtar kendi grubunun rezervasyonlarını, temel
sözleşmedeki bir anahtar temel sözleşmenin rezervasyonlarını iptal eder. Diğer her şey `404 ERR_BOOKING_NOT_FOUND`'dur.
İptal ücretlerini API hesaplamaz.
Her iptal tur operatöründe zaman ve iptal eden anahtarın kimliğiyle (key_id)
kaydedilir ve onun rezervasyon görünümünde gösterilir; bir
tekrar (`alreadyReleased`) ikinci bir kayıt oluşturmaz.

Yanıt: `released` (`true`), `alreadyReleased` (`true` = zaten iptal edilmişti).

```http
### stornieren
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"idemKey": "BEISPIEL-0001"}
```

```json
{"released": true, "alreadyReleased": false}
```

Tekrarlamak güvenlidir:

```http
### stornieren-wiederholt
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"idemKey": "BEISPIEL-0001"}
```

```json
{"released": true, "alreadyReleased": true}
```

Rezervasyon `status: released` ile okunabilir kalır:

```http
### buchung-lesen-storniert
GET {{baseUrl}}/v1/booking?ref={{buchungsRef}}
X-Api-Key: {{apiKey}}
```

```json
{
  "reference": "{{buchungsRef}}",
  "customerReference": "KUNDE-4711",
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "status": "released",
  "bookedAt": "{{*}}",
  "updatedAt": "{{*}}",
  "metadata": {"correlationId": "kette-0815", "vermittler": "Filiale 12"},
  "totalCents": 18000,
  "currency": "EUR",
  "board": "RO"
}
```

İptal edilmiş bir `idemKey` tüketilmiştir. Yeni bir rezervasyon için yeni bir anahtar kullanın:

```http
### buchen-nach-storno
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0001"
}
```

```json
{"errorCode": "ERR_IDEM_KEY_RELEASED", "message": "idemKey gehoert zu einer stornierten Buchung — fuer einen neuen Verkauf einen neuen idemKey verwenden"}
```

```http
### stornieren-unbekannt
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"idemKey": "GIBT-ES-NICHT"}
```

```json
{"errorCode": "ERR_BOOKING_NOT_FOUND", "message": "keine Buchung mit diesem idemKey"}
```

---

## 7. Hata kataloğu
<!-- de:6f7561db7012 -->

Sütun „Çağıran“: **İsteği düzeltin** = tekrarlamayın, hata istektedir.
**Tekrarlayın** = aynı isteği daha sonra bir kez daha gönderin (`Retry-After` varsa en erken
o kadar saniye sonra). **Bildirin** = tur operatörüne/işletmeciye bildirin, tekrarlamak işe
yaramaz. Bir kod birden fazla endpoint'te görülebilir; durum ve anlam aynı kalır.

### 7.1 Erişim ve iletim
<!-- de:d24a8fb6b649 -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_UNAUTHORIZED` | 401 | Anahtar eksik, bilinmiyor veya iptal edilmiş | Anahtarı kontrol edin, tekrarlamayın (tekrarlar geciktirilir, Bölüm 8) |
| `ERR_TENANT_SUSPENDED` | 403 | Anahtar geçerli, tur operatörü askıya alınmış | Tur operatörüne sorun, tekrarlamayın |
| `ERR_KEY_GROUP_INACTIVE` | 403 | Anahtarın müşteri grubu devre dışı | Tur operatörüne sorun |
| `ERR_MODE_MISMATCH` | 403 | `X-TourAPI-Require-Mode` anahtarınkinden farklı bir mod istiyor (ör. test ortamında canlı anahtar); hiçbir şey yürütülmedi (Bölüm 11.1) | Anahtarı değiştirin, tekrarlamayın |
| `ERR_SCENARIO_NOT_ALLOWED` | 422 | Canlı anahtarla, bilinmeyen bir senaryoyla veya etkisiz olduğu bir endpoint'te `X-TourAPI-Sandbox-Scenario` (Bölüm 11.3) | İsteği düzeltin |
| `ERR_METHOD_NOT_ALLOWED` | 405 | yanlış HTTP metodu | İsteği düzeltin |
| `ERR_BAD_REQUEST` | 400 | Body JSON değil, çok büyük (> 1 MiB), `ref` eksik, `priceCheck.tolerancePercent` negatif, `priceCheck.currency` 3 karakterden uzun (daha kısa veya bilinmeyen: `422 ERR_CURRENCY_NOT_AVAILABLE`); Content API: `since` eksik, parametre birden fazla, `lang` 5'ten fazla veya yinelenen dil içeriyor | İsteği düzeltin |
| `ERR_UNKNOWN_FIELD` | 422 | `occupancy` içinde (sorgu) veya herhangi bir yerde (`/v1/search/open`, `/v1/search/open/dates`, `/v1/book`, `/v1/cancel`, `/v1/export/edf/ack` body'si) bilinmeyen alan; `/v1/export/edf/*` ve `/v1/limits` üzerinde bilinmeyen query parametresi | İsteği düzeltin |
| `ERR_OPEN_SEARCH_NOT_ALLOWED` | 403 | anahtarın açık arama ve tarih matrisi yetkisi yok (4a.1; yeni anahtarlarda kapalı; `GET /v1/limits` gösterir) | Tur operatörüne sorun |
| `ERR_DESTINATION_NOT_ALLOWED` | 403 | açık arama: hedef (`destinations[i]`) veya otel (`hotels[i]`, tarih matrisi: `hotel`) arama profilinin izin verilen hedeflerinin dışında | Hedefi arama profilinden alın (`GET /v1/limits`) |

```http
### fehler-ohne-key
POST {{baseUrl}}/v1/price
Content-Type: application/json

{}
```

```json
{"errorCode": "ERR_UNAUTHORIZED", "message": "fehlender/ungueltiger API-Key"}
```

```http
### fehler-key-widerrufen
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{gesperrterKey}}

{}
```

```json
{"errorCode": "ERR_UNAUTHORIZED", "message": "fehlender/ungueltiger API-Key"}
```

```http
### fehler-methode
GET {{baseUrl}}/v1/price
X-Api-Key: {{apiKey}}
```

```json
{"errorCode": "ERR_METHOD_NOT_ALLOWED", "message": "nur POST"}
```

```http
### fehler-tippfehler-belegung
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}, {"alter": 8}]}
}
```

```json
{"errorCode": "ERR_UNKNOWN_FIELD", "message": "unbekanntes Feld 'occupancy.travellers[1].alter' — abgelehnt (die Belegung bestimmt den Preis, hier wird nicht geraten)"}
```

### 7.2 İstek (alanlar ve sınırlar)
<!-- de:9383cf69603c -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_BAD_DATE` | 422 | Tarih eksik veya `JJJJ-MM-TT` biçiminde değil (`message` alanı belirtir) | İsteği düzeltin |
| `ERR_EMPTY_STAY` | 422 | `checkOut` ≤ `checkIn` | İsteği düzeltin |
| `ERR_STAY_TOO_LONG` | 422 | 30 geceden fazla | İsteği düzeltin |
| `ERR_STAY_IN_PAST` | 422 | Giriş referans tarihinden önce | İsteği düzeltin |
| `ERR_STAY_TOO_FAR` | 422 | Giriş referans tarihinden 732 günden fazla sonra | İsteği düzeltin |
| `ERR_NO_TRAVELLERS` | 422 | yolcu yok | İsteği düzeltin |
| `ERR_TOO_MANY_TRAVELLERS` | 422 | 20'den fazla yolcu | İsteği düzeltin |
| `ERR_INVALID_AGE` | 422 | Yaş negatif veya 120'nin üzerinde | İsteği düzeltin |
| `ERR_BOARD_MISSING` | 422 | `board` eksik (`/v1/price`, `/v1/search`, `priceCheck`) | İsteği düzeltin |
| `ERR_NOW_MISMATCH` | 422 | `priceCheck.now` referans tarihi değil | Alanı çıkarın |
| `ERR_VALIDATION` | 422 | Metin çok uzun: `idemKey`, `reference` (128), `leadPaxName` (255), `metadata.correlationId` (64); `message` alanı ve sınırı belirtir | İsteği düzeltin |
| `ERR_QUANTITY_INVALID` | 400 | `quantity` 1–1.000.000 dışında | İsteği düzeltin |
| `ERR_INVALID_IDEM_KEY` | 400 | `idemKey` eksik (`/v1/book`, `/v1/cancel`) | İsteği düzeltin |
| `ERR_INVALID_BUCKET` | 400 | `room` eksik (`/v1/book`, `priceCheck` ile ve olmadan) | İsteği düzeltin |
| `ERR_INVALID_STAY` | 400 | Konaklama geçersiz (satışta koruma; API bunu önceden `ERR_BAD_DATE`/`ERR_EMPTY_STAY` ile kontrol eder) | İsteği düzeltin |
| `ERR_BAD_PAGE_SIZE` | 422 | `pageSize` 1–100 (`/v1/search`; açık arama: 1'den arama profiline kadar) veya 1–1000 (`/v1/content/*`) dışında | İsteği düzeltin |
| `ERR_BAD_CURSOR` | 422 | Cursor okunamıyor, değiştirilmiş veya süresi dolmuş (ör. sunucu yeniden başlatıldıktan sonra; açık arama: verilişinden 15 dakika sonra); Content API: `cursor`/`since` okunamıyor, değiştirilmiş veya başka bir anahtardan | `cursor` olmadan yeniden başlayın veya dizini yeniden alın |
| `ERR_CURSOR_MISMATCH` | 422 | Cursor başka bir isteğe veya başka bir anahtara ait (`/v1/search`, `/v1/search/open`) | İsteği düzeltin |
| `ERR_UNKNOWN_DESTINATION` | 422 | `cursor` olmadan `/v1/search` veya `/v1/search/open`: `destination` veya `destinations[i]` için bu anahtara ait otel yok (büyük/küçük harf önemlidir) | Kodu `GET /v1/destinations`'dan alın (4.5) |
| `ERR_BAD_WINDOW` | 422 | açık arama ve tarih matrisi: `arrivalFrom`/`arrivalTo` eksik veya `arrivalTo`, `arrivalFrom`'dan önce | İsteği düzeltin |
| `ERR_BAD_NIGHTS` | 422 | açık arama ve tarih matrisi: `nightsMin`/`nightsMax` eksik, < 1 veya `nightsMin` > `nightsMax` | İsteği düzeltin |
| `ERR_BAD_TARGET` | 422 | açık arama: `destinations` ve `hotels` birlikte, boş liste, boş kod veya arama profili yalnızca tekil hedeflere izin verdiği halde ikisi de yok; tarih matrisi: `hotel` eksik | İsteği düzeltin |
| `ERR_BAD_SORT` | 422 | açık arama: `sort` bilinmiyor (`price`, `pricePerNight`, `hotel`) | İsteği düzeltin |
| `ERR_BAD_FILTER` | 422 | açık arama ve tarih matrisi: boş `boards`/`boardTypes`/`rooms` listesi veya boş giriş, bilinmeyen pansiyon türü, fiyat filtresi negatif veya `minTotalCents` > `maxTotalCents`, `currency` üç büyük harfli bir kod değil; otel ana verilerine göre filtre biçimi dışında (`category`, `regions`, `geo`, 4a.2) – mesaj alanı belirtir | İsteği düzeltin |
| `ERR_WINDOW_TOO_WIDE` | 422 | açık arama veya tarih matrisi: varış penceresi arama profilinin izin verdiğinden geniş (`maxWindowDays` veya `matrixMaxWindowDays`, `message` sınırı belirtir) | pencereyi bölün veya daraltın |
| `ERR_NIGHTS_NOT_ALLOWED` | 422 | açık arama: süre izin verilen sürelerin dışında veya `nightsMax - nightsMin + 1` aralığı çok geniş; tarih matrisi: süre izin verilen sürelerin dışında (`message` sınırı belirtir) | süreyi ayarlayın |
| `ERR_SEARCH_TOO_BROAD` | 422 | açık arama: istekte çok fazla `destinations` veya `hotels` ya da arama kapsamında çok fazla otel; tarih matrisi: `matrixMaxCells` değerinden fazla hücre (`message` sınırı belirtir) | daraltın |
| `ERR_CURRENCY_REQUIRED` | 422 | açık arama: arama kapsamındaki oteller birden fazla sözleşme para biriminde fiyatlandırıyor, `currency` eksik (`message` bunları belirtir) | `currency` belirtin |

```http
### fehler-aufenthalt-zu-lang
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D31}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_STAY_TOO_LONG", "message": "checkOut: Aufenthalt laenger als 30 Naechte"}
```

```http
### fehler-anreise-vergangen
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "board": "RO",
  "checkIn": "{{gestern}}",
  "checkOut": "{{heute}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_STAY_IN_PAST", "message": "checkIn: {{gestern}} liegt vor dem Stichtag {{heute}} (Serverdatum)"}
```

Çok uzun metinler satıştan önce reddedilir, asla kısaltılmaz:

```http
### fehler-text-zu-lang
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
}
```

```json
{"errorCode": "ERR_VALIDATION", "message": "idemKey: zu lang (129 Zeichen, hoechstens 128)"}
```

Konaklama düzeni dışında yanlış yazılmış bir alan reddedilmez, ancak belirtilir – burada
bu yüzden zorunlu alan eksik kalır ve yanıt her ikisini de söyler:

```http
### fehler-tippfehler-feld
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "board": "RO",
  "checke": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_BAD_DATE", "message": "checkIn: fehlt (Pflichtfeld, JJJJ-MM-TT)",
 "warnings": ["unbekanntes Feld 'checke' — ignoriert (Tippfehler?)"]}
```

Başarılı bir yanıttaki uyarılar:

```http
### hinweise-now-und-waehrung
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "EZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "now": "{{gestern}}",
  "currency": "USD",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{
  "room": "EZ",
  "currency": "EUR",
  "rounding": {"mode": "Commercial", "decimalPlaces": 2, "scope": "Person"},
  "totalCents": 12500,
  "perTravellerCents": [12500],
  "breakdown": [
    {"chargeType": "BaseCharge", "code": "Base", "traveller": 0, "night": 0, "amountExact": "70.00", "amountCents": 7000},
    {"chargeType": "BaseCharge", "code": "ExtraDay", "traveller": 0, "night": 1, "amountExact": "55.00", "amountCents": 5500}
  ],
  "availability": {"configured": true, "available": true, "minFree": 5},
  "warnings": [
    "now: '{{gestern}}' ignoriert — Stichtag ist das Serverdatum {{heute}}",
    "currency: angefragt 'USD', der Vertrag rechnet in 'EUR' (keine Umrechnung)"
  ]
}
```

### 7.3 Stok, fiyat, satış
<!-- de:48d12ea5845b -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_HOTEL_NOT_FOUND` | 404 | Otel bu anahtar için yok (ayrıca: başka tur operatörü, yayınlanmamış, müşteri grubunun özel fiyatı hesaplanamıyor – bkz. 1.1) | İsteği düzeltin; aksi halde bilinen bir otelde tur operatörünü bilgilendirin |
| `ERR_ROOM_NOT_FOUND` | 404 | Oda bu otelde yok (tarih matrisi: `rooms[i]`) | İsteği düzeltin |
| `ERR_BOOKING_NOT_FOUND` | 404 | Rezervasyon bilinmiyor veya bu anahtar için görünür değil | Referansı/anahtarı kontrol edin |
| `ERR_REFERENCE_AMBIGUOUS` | 409 | kendi `reference` değeriniz birden fazla rezervasyonla eşleşiyor (`/v1/booking`); `message` `TA-…` referanslarını belirtir (test anahtarı: `SB-…`) | TourAPI referansıyla okuyun; kendi referanslarınızı benzersiz tutun |
| `ERR_TENANT_NOT_FOUND` | 404 | Tur operatörü (artık) aktif değil, yalnızca satış/iptal | Bildirin |
| `ERR_BOARD_NOT_OFFERED` | 422 | Pansiyon tipi sunulmuyor (kod tam olarak sözleşmedeki gibi; `/v1/price`, `/v1/prices`, `priceCheck`; açık arama: `boards[i]` arama kapsamındaki hiçbir otel tarafından sunulmuyor; tarih matrisi: `boards[i]` otelde yok veya hiçbir pansiyon tipi `boards`/`boardTypes` ile uyuşmuyor); aramada `diagnostics.reasons` veya `coverage.reasons` içinde neden | İsteği düzeltin |
| `ERR_OCCUPANCY_NOT_ALLOWED` | 422 | Konaklama düzeni hiçbir odaya (veya istenen odaya) uymuyor | farklı konaklama düzeni/farklı oda |
| `ERR_STAY_LENGTH_NOT_ALLOWED` | 422 | odanın bir satış kuralı farklı bir konaklama süresi gerektiriyor (3.4); aramada `diagnostics.reasons` içinde neden | Süreyi değiştirin |
| `ERR_ARRIVAL_DAY_NOT_ALLOWED` | 422 | bir satış kuralı farklı bir giriş/çıkış haftanın günü gerektiriyor (3.4) | Seyahat günlerini kaydırın |
| `ERR_TRAVEL_DATES_NOT_ALLOWED` | 422 | konaklama bir kuralın satış penceresi dışında (3.4) | farklı dönem |
| `ERR_BOARD_NOT_ALLOWED` | 422 | pansiyon tipi bu konaklama için satılmıyor (3.4) | farklı pansiyon tipi |
| `ERR_LEAD_TIME_NOT_ALLOWED` | 422 | giriş, bir satış kuralının release süresi içinde; referans tarihinden itibaren sayılır (3.4) | daha geç giriş |
| `ERR_BOARD_NOT_AVAILABLE` | 422 | pansiyon tipi bu yolcu grubuna satılmıyor, ek ücreti farklı bir bileşim gerektiriyor (3.5); `/v1/prices` içinde `rooms[].errors[]` kaydı, aramada `diagnostics.reasons` içinde neden | farklı pansiyon tipi/konaklama düzeni |
| `ERR_ROOM_RESTRICTION_INVALID` | 422 | sözleşmedeki bir satış kuralı değerlendirilemiyor | Bildirin |
| `ERR_NO_SECTION` | 422 | bir gece için fiyat yok (sezon sözleşmede yok); başka bir oda hesaplanıyorsa `/v1/prices` içinde `rooms[].errors[]` kaydı, `room` olmadan `/v1/price`'ta `warnings` içinde, aramada `diagnostics.roomErrors` içinde | farklı dönem |
| `ERR_NO_PRICE` | 422 | istek için rezerve edilebilir oda yok, daha kesin bir neden olmadan | farklı dönem/farklı konaklama düzeni |
| `ERR_OCCUPANCY_NIGHT_UNCOVERED` | 422 | Sözleşmenin konaklama düzeni kuralları bir geceyi kapsamıyor | Bildirin |
| `ERR_OCCUPANCY_INCONSISTENT_MCA` | 422 | Minimum doluluk konaklama içinde değişiyor (desteklenmiyor) | daha kısa dönem veya bildirin |
| `ERR_OCCUPANCY_INCONSISTENT_CHILDREN` | 422 | bir yolcu konaklama içinde bir kez çocuk, bir kez yetişkin (odanın çocuk yaş aralığı sezona göre değişiyor; desteklenmiyor) | daha kısa dönem veya bildirin |
| `ERR_OCCUPANCY_INCONSISTENT_INFANTS` | 422 | bir bebek konaklama içinde bir kez doluluğa sayılıyor, bir kez sayılmıyor (odanın konaklama düzeni kuralı sezona göre değişiyor) ve bir ek ücret veya indirim kişi sayısına bağlı; belirsiz | daha kısa dönem veya bildirin |
| `ERR_CHILDREN_ORDER_MISSING` | 422 | sözleşme önce en büyük mü yoksa en küçük çocuğun mu sayılacağını belirlemiyor ve bu çocuklar için bu bir fiyat farkı yaratıyor (sözleşme hatası) | Bildirin |
| `ERR_INVALID_AMOUNT` | 422 | sözleşmedeki bir tutar veya yüzde okunamıyor | Bildirin |
| `ERR_AMOUNT_OVERFLOW` | 422 | fiyat gösterilebilir sent aralığını aşıyor (sözleşme hatası) | Bildirin |
| `ERR_CURRENCY_NOT_AVAILABLE` | 422 | `priceCheck.currency` sözleşme para birimine uymuyor veya sözleşme para birimi bilinmiyor; açık aramada `coverage.reasons` içinde neden (otel istenenden başka veya bilinmeyen bir para biriminde fiyatlandırıyor); tarih matrisi: aynısı 422 olarak | İsteği düzeltin veya bildirin |
| `ERR_PRICE_DRIFT` | 409 | güncel fiyat `priceCheck`'ten sapıyor | yeni fiyatı gösterin, yeni `expectedCents` ile rezervasyon yapın |
| `ERR_SOLD_OUT` | 422 | Gece dolu | tekrarlamayın |
| `ERR_STOP_SALE` | 422 | Satış durdurma | tekrarlamayın |
| `ERR_INVENTORY_CLOSED` | 422 | Gece kapalı veya yalnızca talep üzerine | tekrarlamayın |
| `ERR_NO_INVENTORY` | 422 | en az bir gece için kapasite tanımlı değil (rezervasyon ve arama nedeni aynı) | tekrarlamayın |
| `ERR_NOT_AVAILABLE` | – | yalnızca aramada neden olarak: her gecenin kapasitesi var, ancak her biri açık değil (rezervasyon: `ERR_SOLD_OUT`, `ERR_STOP_SALE`, `ERR_INVENTORY_CLOSED`) | – |
| `ERR_OUTSIDE_PRICE_FILTER` | – | yalnızca neden olarak: açık aramada otelin her teklifi, tarih matrisinde hücrenin her teklifi `minTotalCents`/`maxTotalCents` dışında | – |
| `ERR_NO_CATEGORY` | – | yalnızca neden olarak (açık arama; tarih matrisinde otelin her hücresi): `category` filtresi, otelin otel ana verilerinde resmi kategorisi yok | Tur operatörü: kategoriyi girin |
| `ERR_NO_REGION` | – | yalnızca neden olarak (açık arama; tarih matrisinde otelin her hücresi): `regions` filtresi, otelin otel ana verilerinde bölgesi yok | Tur operatörü: bölgeyi girin |
| `ERR_NO_GEO` | – | yalnızca neden olarak (açık arama; tarih matrisinde otelin her hücresi): `geo` filtresi, otelin otel ana verilerinde koordinatı yok | Tur operatörü: koordinatları girin |
| `ERR_GROUP_LIMIT` | 422 | Müşteri grubunun tahsisi tükenmiş veya oda ve gece için mevcut değil | tekrarlamayın |
| `ERR_IDEMPOTENCY_MISMATCH` | 409 | `idemKey` zaten farklı rezervasyon verileriyle kullanılmış | Çağıranın hatası: benzersiz anahtarlar verin |
| `ERR_IDEM_KEY_RELEASED` | 409 | `idemKey` iptal edilmiş bir rezervasyona ait | yeni `idemKey` kullanın |
| `ERR_SANDBOX_LIMIT` | 422 | Test anahtarı: bu erişimin 5.000'den fazla açık test rezervasyonu var (Bölüm 11.2) | Test rezervasyonlarını iptal edin |

```http
### fehler-verpflegung-nicht-angeboten
POST {{baseUrl}}/v1/prices
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "boards": ["AI"],
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_BOARD_NOT_OFFERED", "message": "boards: Verpflegung 'AI' wird nicht angeboten"}
```

Aynısı `/v1/price` için de geçerlidir – odanın sunmadığı bir pansiyon tipi asla
pansiyon tipi olmayan fiyattan hesaplanmaz:

```http
### fehler-verpflegung-preis
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "board": "AI",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_BOARD_NOT_OFFERED", "message": "board: Verpflegung 'AI' wird nicht angeboten"}
```

### 7.4 Yük ve işletim
<!-- de:f971621e9969 -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_RATE_LIMITED` | 429 | bu anahtardan çok fazla istek (Bölüm 8); açık arama ve tarih matrisi: arama profilinin istek hızı aşıldı (ikisi birlikte) | `Retry-After` sonrasında tekrarlayın |
| `ERR_SEARCH_BUSY` | 429 | tur operatörünün çok fazla eşzamanlı araması (açık arama: arama profiline göre anahtarın da) | `Retry-After` (1 sn) sonrasında tekrarlayın |
| `ERR_SEARCH_TIMEOUT` | 503 | Arama süre sınırını (10 sn) aştı | daraltın (`destination`/`destinations`, daha küçük pencere), sonra tekrarlayın |
| `ERR_PRICE_TIMEOUT` | 503 | `room` olmadan `/v1/price` veya `/v1/prices` süre sınırını (2 sn) aştı | `room` ile `/v1/price` çağrısına geçin (`/v1/prices` için sınır her zaman geçerlidir, `room` ve `boards` ile de), sonra tekrarlayın |
| `ERR_BOOKING_DISABLED` | 503 | Satış/iptal bu düğümde etkin değil (test anahtarı: sandbox etkin değil – bir test anahtarı asla canlı rezervasyon yapmaz) | Tekrarlayın, kalıcıysa: bildirin |
| `ERR_BOOKING_BUSY` | 503 | Rezervasyon/iptal aynı oteldeki eşzamanlı işlemler nedeniyle gerçekleşmedi, hiçbir şey rezerve veya iptal edilmedi | `Retry-After` (1 sn) sonrasında **aynı** `idemKey` ile tekrarlayın |
| `ERR_INTERNAL` | 500 | dahili hata, ör. veritabanına erişilemiyor (hesaplama çekirdeğinde ihlal edilen bir invaryant, sorgu endpoint'lerinde 422 olarak gelir); açık arama: sonuç kanıtlanamıyor (4a.4) | Ara vererek tekrarlayın; `/v1/book`'ta **aynı** `idemKey` ile |
| `ERR_INVENTORY_DRIFT` | 500 | Kontenjan invaryantı ihlal edildi, hiçbir şey satılmadı | Bildirin |
| `ERR_INVENTORY_STATUS_UNKNOWN` | 500 | kontenjanda bilinmeyen günlük durum, hiçbir şey satılmadı | Bildirin |
| `ERR_RELEASE_DRIFT` | 500 | İptal, kontenjan invaryantı nedeniyle engellendi, hiçbir şey iptal edilmedi | Bildirin |

### 7.5 Sözleşme verileri (tur operatöründeki hatalar)
<!-- de:90be264a1901 -->

Bu kodlar, otelin sözleşmesi TourAPI'nin hesaplamadığı (veya bu şekilde hesaplamadığı) bir kural
içerdiğinde hesaplama çekirdeğinden gelir. Sözleşme önceden kontrol edildiği için yayınlandıktan sonra
görülmemeleri gerekir. Durum her zaman **422**. Çağıran: **Bildirin** (otel,
dönem ve `message` ile); aramada `diagnostics.reasons` içinde neden olarak
(otel düşer) veya `diagnostics.roomErrors` içinde (tek oda atlanır) görünürler.
`room` olmadan `/v1/price`'ta, başka bir oda hesaplanabildiği sürece etkilenen oda atlanır ve
`warnings` içinde belirtilir; `/v1/prices` etkilenen pansiyon tiplerini `rooms[].errors[]`
içinde belirtir ve geri kalanını fiyatlar (Bölüm 3.2). `ERR_NEGATIVE_TRAVELLER_PRICE` ve
`ERR_NEGATIVE_PERCENT_BASE` konaklama düzenine ve pansiyon tipine bağlıdır: Yazma kapısı bunlara karşı uyarır,
ancak sözleşmeye izin verir; bu nedenle yayınlandıktan sonra da gelebilirler. `/v1/prices`
o zaman yalnızca etkilenen pansiyon tipini atlar (`rooms[].errors[]`, Bölüm 3.2).

| Kod | Anlamı |
|---|---|
| `ERR_NO_BASECHARGE` | Temel fiyat eksik |
| `ERR_SECTION_BAD_DATE`, `ERR_BOARD_BAD_DATE`, `ERR_OCCUPANCY_BAD_DATE` | sözleşmede geçersiz tarih |
| `ERR_OCCUPANCY_INCOMPLETE` | Konaklama düzeni kuralı eksik |
| `ERR_AMBIGUOUS_BOARDCHARGE` | iki pansiyon tipi ek ücreti aynı gecede aynı kişiye uygulanabiliyor; hangisinin geçerli olduğu belirsiz (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_AMBIGUOUS_BASECHARGE`, `ERR_AMBIGUOUS_SECTION` | bir sezonda aynı tipte iki temel fiyat veya aynı gün için iki sezon; hangi fiyatın geçerli olduğu belirsiz (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_AMBIGUOUS_FREENIGHT` | aynı konaklama için iki ücretsiz gece teklifi devreye girebiliyor; ikincisinin hangi geceleri bağışladığı belirlenmemiş (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_UNSUPPORTED_FREENIGHT`, `ERR_UNSUPPORTED_REDUCTION_MODE` | TourAPI'nin hesaplamadığı bir biçimde ücretsiz gece teklifi (ör. yüzde yerine sabit tutar, kişi kısıtlaması, „büyük/küçük“ gece seçimi) |
| `ERR_NEGATIVE_TRAVELLER_PRICE` | tüm ek ücret ve indirimlerden sonra bir yolcu 0'dan az öderdi (ör. hiçbir ücreti olmayan bir çocuğa kişi başı sabit indirim veya %100'ü aşan üst üste indirimler). Bir yolcu asla 0'dan az ödemez; `message` yolcuyu ve ek ücreti/indirimi belirtir. Yüzde indirimleri, yolcunun çocuk/kişi indiriminden sonra borçlu olduğu tutar üzerinden hesaplanır ve hatayı tek başına tetiklemez |
| `ERR_NEGATIVE_PERCENT_BASE` | bir çocuk/kişi indirimi (sabit tutar), etki ettiği günlük fiyattan veya pansiyon tipinden büyük; bunun üzerindeki bir yüzde indirimi ek ücrete dönüşür, bir ücretsiz gece (ör. „7=6“) konaklamayı pahalılaştırırdı. `message` ek ücreti/indirimi veya ücretsiz geceyi, yolcuyu ve geceyi belirtir |
| `ERR_UNSUPPORTED_OCCUPANCY_PRICEBLOCK` | Doluluğa göre temel fiyat (harici bir teslimattan, ör. “tam 1 kişi” / “2 kişi ve üzeri”) TourAPI'nin hesaplamadığı bir biçimde — ya da böyle bir odada bebek seyahat ediyor (tedarikçi odayı bebekle satmıyor) |
| `ERR_UNSUPPORTED_GUESTCHARGE_OBJECT` | Bir oda fiyatına (obje fiyatı) kişi indirimi (ör. çocuk indirimi): etki edebileceği kişi başı bir fiyat yok (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_UNSUPPORTED_COMBIGROUP` | bir teklif 0 numaralı kombinasyon grubunda münhasır; 0 grubu EDF'de grubu olmayan tüm teklifleri de temsil eder, bir EDF alıcısı farklı hesaplardı (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_COMPATIBLE_WITH_INVALID` | bir teklifin “yalnızca şu gruplarla birleştirilebilir” listesi belirsiz: bir grup iki kez, 0..2147483647 dışında ya da “grupta yalnızca biri” ile birlikte (yazma kapısı buna izin vermez, yalnızca eski kayıtlar) |
| `ERR_CALCMODE_MISSING`, `ERR_CALCMODE_UNSUPPORTED` | Odanın hesaplama türü eksik veya desteklenmiyor |
| `ERR_BASE_BOARD_INVALID`, `ERR_BASE_BOARD_CHARGED` | Odanın temel pansiyonu (taban fiyata dahil pansiyon) geçersiz veya kendi ek ücretini taşıyor (yazma kapısı buna izin vermez, yalnızca eski veri) |
| `ERR_MINCHARGEDPERSONS_MISSING`, `ERR_INVALID_MIN_CHARGED_PERSONS` | Minimum ödeme yapan kişi sayısı eksik veya geçersiz |
| `ERR_INVALID_ENUM`, `ERR_INVALID_WEEKDAY_MASK` | geçersiz numaralandırma değeri veya haftanın günü maskesi |
| `ERR_MISSING_APPLIANCE_TYPE`, `ERR_MISSING_AMOUNT_OR_PERCENT`, `ERR_MISSING_BOARD_CODE`, `ERR_AMOUNT_PERCENT_CONFLICT`, `ERR_DUPLICATE_APPLIANCE_CODE`, `ERR_EXTRA_TYPE_INVALID` | Ek ücret veya indirim eksik ya da çelişkili |
| `ERR_EXTRACALC_UNSUPPORTED`, `ERR_UNSUPPORTED_APPLIANCE_TYPE`, `ERR_UNSUPPORTED_APPLYTOBOARD`, `ERR_UNSUPPORTED_EXTRA_REF`, `ERR_UNSUPPORTED_GUESTCHARGE_LINK`, `ERR_UNSUPPORTED_GUESTCHARGE_TYPE`, `ERR_UNSUPPORTED_INVERT`, `ERR_UNSUPPORTED_MANDATORY`, `ERR_UNSUPPORTED_MCA_AGEWINDOW`, `ERR_UNSUPPORTED_MCA_RANGE`, `ERR_UNSUPPORTED_RESTRICTION`, `ERR_UNSUPPORTED_VARMCP_PERSTAY` | TourAPI'nin hesaplamadığı sözleşme kuralı |

### 7.6 EDF teslimatı (`/v1/export/edf/*`, Bölüm 10)
<!-- de:f4326be3cefd -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_EXPORT_BAD_CURSOR` | 400 | `epoch`/`since` eksik veya okunamıyor, `until` geçerli bir zincir anahtarı değil, `max_bytes` ≥ 65536 bir sayı değil, parametre iki kez verilmiş, `until` olmadan bir durumun ortasında sayfa, `full`'ün `epoch`/`since`/`until` olmadan sonraki sayfası | İsteği düzeltin veya zinciri yeniden başlatın |
| `ERR_EXPORT_NOT_ALLOWED` | 403 | Dışa aktarım yetkisi olmayan anahtar | Tur operatörüne sorun, tekrarlamayın |
| `ERR_EXPORT_EPOCH` | 409 | `epoch` uymuyor (feed yeniden oluşturuldu) veya durum (`since`, `until`, onayın `seq` değeri) güncel teslimat durumunun **üzerinde** (son teslim edilen durumdan büyük) | `full` çekin |
| `ERR_EXPORT_CURSOR_EXPIRED` | 410 | Durum saklama süresinden eski | `full` çekin |
| `ERR_EXPORT_NOT_READY` | 503 | Bu anahtar için teslimat henüz oluşturulmadı, geçici olarak güncel değil (teslimat 2 dakikadan fazla geride) veya düğümde yapılandırılmamış | `Retry-After` sonrasında tekrarlayın; kalıcıysa: bildirin |

Ayrıca `/v1/export/edf/*` üzerinde: 7.1'deki `401`/`403` (`ERR_TENANT_SUSPENDED`,
`ERR_KEY_GROUP_INACTIVE`), `422 ERR_UNKNOWN_FIELD` (bilinmeyen parametre veya onay alanı)
ve döngü için `429 ERR_RATE_LIMITED` (10.3; `full`'de 3600 sn'ye kadar `Retry-After` – körü körüne
beklemeyin, bir sonraki döngüyü planlayın).

### 7.7 Otel içerikleri (`/v1/content/*`, Bölüm 12)
<!-- de:14adc4697ab3 -->

| Kod | Durum | Anlamı | Çağıran |
|---|---|---|---|
| `ERR_CONTENT_NOT_ALLOWED` | 403 | İçerikler tur operatörü için etkin değil (ayrıca: test ortamına bu kurulumda hizmet verilmiyor) veya anahtarın içerik yetkisi yok | Tur operatörüne sorun, tekrarlamayın |
| `ERR_LANGUAGE_NOT_OFFERED` | 422 | `lang`, tur operatörünün içerik dillerinden olmayan bir dil içeriyor (katalogda: etiket dili değil); sunulanlar `warnings` içindedir | İsteği düzeltin |
| `ERR_CONTENT_CURSOR_EXPIRED` | 410 | `since` akışın saklama ufkunun (30 gün) öncesinde | Dizini yeniden alın, onun `feedToken`'ı ile devam edin |
| `ERR_CONTENT_NOT_READY` | 503 | İçerikler veya görsel adresleri bu düğümde kurulu değil | `Retry-After` sonrasında tekrarlayın; kalıcıysa: Bildirin |

Ayrıca `/v1/content/*` üzerinde: 7.1'den `401`/`403`, `404 ERR_HOTEL_NOT_FOUND` (otel
anahtarın dizininde değil), `400 ERR_BAD_REQUEST`, `422 ERR_BAD_PAGE_SIZE`, `ERR_BAD_CURSOR`
ve `429 ERR_RATE_LIMITED` (anahtar başına hız, Bölüm 8). Bilinmeyen
sorgu parametreleri yok sayılır ve `warnings` içinde belirtilir.

---

## 8. Adalet: hız sınırı ve arama kapısı
<!-- de:208b309ad0eb -->

| Kilit | Sınır (varsayılan ayar) | Yanıt |
|---|---|---|
| API anahtarı başına istek | saniyede 200, kısa süreliğine 400'e kadar (token bucket) | `429 ERR_RATE_LIMITED` + `Retry-After` |
| tur operatörü başına eşzamanlı arama | 8 (hesaplama süresi: çekirdeklerin dörtte biri, en az 1) | `429 ERR_SEARCH_BUSY` + `Retry-After: 1` |
| bir aramanın hesaplama süresi | 10 sn | `503 ERR_SEARCH_TIMEOUT` |
| `room` olmadan `/v1/price` ve `/v1/prices` hesaplama süresi | 2 sn | `503 ERR_PRICE_TIMEOUT` |

- Sınırlar **sunucu düğümü başına** geçerlidir. Döngülere ve yük zirvelerine karşı bir korumadır,
  faturalandırılabilir bir kota değildir. İşletmeci bunları değiştirebilir.
- `Retry-After` tam saniye cinsindendir (en az 1). Öncesinde tekrarlamayın; sonrasında
  aynı istekle (`/v1/book`'ta aynı `idemKey` ile) tekrarlayın.
- `Retry-After` dolmadan yeniden gönderen ve tekrar reddedilen,
  `429` yanıtını gecikmeli alır (bildirilen bekleme süresinin sonuna kadar, en fazla 1 sn). Bekleme süresi
  API anahtarı başına geçerlidir: aynı anahtarla birden fazla süreç paralel çalışıyorsa,
  gecikme bir `429` sonrasında göndermeye devam eden herkesi etkiler, kaç bağlantıyla olursa olsun.
  Bağlantı açık kalır.
- Reddedilen anahtarlar (`401`, `403 ERR_TENANT_SUSPENDED`, `403 ERR_KEY_GROUP_INACTIVE`)
  gönderici IP'si başına sayılır: kısa sürede 20 retten sonra saniyede yalnızca 5 istek hemen yanıtlanır,
  diğerleri gecikmeli (en fazla 1 sn), kaç bağlantıyla olursa olsun. Durum ve
  yanıt aynı kalır. Geçerli anahtarlı istekler bundan asla etkilenmez.
- Bilinmeyen yollar (`404`) ve yanlış metotlar (`405 ERR_METHOD_NOT_ALLOWED`), reddedilen anahtarlarla
  aynı gönderici IP'si başına sınıra dahil edilir.
- `/v1/health`'in gönderici IP'si başına kendi sınırı vardır: 50 çağrı hemen, sonra saniyede 10
  hemen, diğerleri gecikmeli (en fazla 1 sn). Yanıt her zaman
  güncel durumdur, asla bir ret değildir.
- Reddedilen istekler kullanım olarak sayılmaz.
- Kesilmiş arama sayfaları hata değildir, bkz. 4.3.

Kısıtlama şöyle görünür (örnekler 10 sn'de 1 isteğe izin veren bir örneğe karşı çalışır):

```http
### drossel-erste-anfrage
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "EZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```http
### drossel-zweite-anfrage
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{apiKey}}

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "EZ",
  "board": "RO",
  "checkIn": "{{D0}}",
  "checkOut": "{{D2}}",
  "occupancy": {"travellers": [{"age": 40}]}
}
```

```json
{"errorCode": "ERR_RATE_LIMITED", "message": "Anfrage-Rate dieses API-Keys ueberschritten"}
```

Buna ait header: `Retry-After: 10`.

---

## 9. Idempotency ve eşzamanlılık
<!-- de:d7acc022c4ba -->

**Rezervasyon:**

- `idemKey` zorunludur ve çağırana aittir. Tur operatörü **ve** anahtar çevresi başına
  (müşteri grubu veya temel sözleşme) geçerlidir: İki müşteri grubu birbirini
  etkilemeden aynı anahtarı kullanabilir.
- Aynı `idemKey`, aynı rezervasyon verileri (otel, oda, konaklama, miktar) = aynı
  rezervasyon: `200`, `alreadyBooked: true`, aynı referans. Bir tekrarın `reference`, `metadata`,
  `leadPaxName` ve `priceCheck` değerleri karşılaştırılmaz ve
  devralınmaz – ilk rezervasyon geçerlidir.
- Aynı `idemKey`, farklı rezervasyon verileri: `409 ERR_IDEMPOTENCY_MISMATCH`.
- İptal edilmiş `idemKey`: `409 ERR_IDEM_KEY_RELEASED`.
- Idempotency kontrolü referans tarihi, sınırlar ve `priceCheck`'ten **önce** gelir: Rezervasyon
  mevcutsa, tekrar onu gece yarısından sonra da (giriş artık
  geçmişte, `priceCheck.now` artık referans tarihi değil) veya bir fiyat değişikliğinden sonra da onaylar.
  Yalnızca yeni bir `idemKey` bu kontrollerden geçer. İsteğin biçim hataları (bilinmeyen
  alanlar, eksik zorunlu alanlar, çok uzun metinler) yine önceden reddedilir.
- Bir `idemKey`'in ilk rezervasyonu hâlâ sürüyorsa, bir tekrar onun sonucunu bekler
  ve ardından onu onaylar (`200`, `alreadyBooked: true`); ilki başarısız olursa
  tekrar yeni bir rezervasyon olarak çalışır. Bekleme çok uzun sürerse: `Retry-After` ile
  `503 ERR_BOOKING_BUSY` – aynı `idemKey` ile tekrarlayın.
- Aynı odanın iki eşzamanlı rezervasyonu kontenjanı asla aşırı dolduramaz: Her
  gece atomik olarak sayılır ve bir rezervasyon ya tüm geceleri alır ya hiçbirini.
- Rezervasyon veya iptal aynı oteldeki eşzamanlı işlemler nedeniyle gerçekleşmezse:
  `Retry-After` ile `503 ERR_BOOKING_BUSY` (Bölüm 7.4). Hiçbir şey rezerve veya
  iptal edilmemiştir; bekleme süresinden sonra aynı `idemKey` ile tekrarlayın.
- Bir otel, onun için bir rezervasyon sürerken silinirse sıra geçerlidir: önce
  rezervasyon yapıldıysa, o kalır; aksi halde `404 ERR_HOTEL_NOT_FOUND`, hiçbir şey rezerve edilmez.
- Zaman aşımı veya `5xx` sonrasında rezervasyon yapılıp yapılmadığı belirsizdir: **aynı
  `idemKey` ile tekrarlayın**, asla yenisiyle değil.

**İptal:**

- Rezervasyonun `idemKey` değeri üzerinden adreslenir. Çift iptal bir başarıdır
  (`alreadyReleased: true`). Aynı rezervasyonun eşzamanlı iptalleri kontenjanı
  tam olarak bir kez iade eder.

---

## 10. EDF teslimatı (önbellek dışa aktarımı)
<!-- de:8e14c6d07d7c -->

Amaç: Bir alıcı, anahtarının tüm otellerinin fiyat ve müsaitliklerini
kendi önbelleğinde EDF dosyaları olarak tutar ve rezervasyondan önce canlı sorgular (`/v1/price`, sonra
`/v1/book`). Bağlayıcı olan her zaman `/v1/book`'tur; önbellek bir tekliftir, stok değildir.
Tam teslimat sözleşmesi (manifestin kanonik biçimi, sınırlar) işletmeciden talep üzerine
alınabilir.

| Metot/yol | Yanıt |
|---|---|
| `GET /v1/export/edf/full[?max_bytes=N]` | Tam durumu içeren `200` zip (büyük stoklarda ilk sayfa) |
| `GET /v1/export/edf/full?epoch=E&since=S&until=K[&max_bytes=N]` | Tam durumun sonraki sayfasını içeren `200` zip |
| `GET /v1/export/edf/changes?epoch=E&since=S[&until=K][&max_bytes=N]` | `S` durumundan sonraki tüm değişiklikleri içeren `200` zip; yeni bir şey yoksa `204` |
| `POST /v1/export/edf/ack` `{"epoch": E, "seq": T}` | `204`; „işlendi“ bildirir (yalnızca tur operatöründeki izleme için) |

- **Kapsam yalnızca anahtar üzerinden:** Neyin teslim edileceğini anahtar belirler – bu anahtar için
  `/v1/search` ve `/v1/price` ile aynı oteller ve fiyatlar (temel sözleşme, kendi fiyatıyla müşteri grubu,
  yalnızca tahsisli otellerle kontenjan grubu). Tur operatörünü veya grubu seçen bir parametre
  yoktur; bilinmeyen bir parametre `422 ERR_UNKNOWN_FIELD`'dır.
- **Dışa aktarım yetkisi:** anahtar başına etkinleştirilir (tur operatörü yöneticisi, konsolda
  anahtar satırında „Export erteilen“); yetki yoksa `403 ERR_EXPORT_NOT_ALLOWED`.
- **Yalnızca yayınlananlar:** Taslaklar teslimatı asla değiştirmez. Değişiklikler (yayınlama,
  satış durdurma, rezervasyon, kampanya, tahsis) en geç yaklaşık bir dakika sonra feed'de yer alır.

### 10.1 Paket
<!-- de:646450ed51d0 -->

İlk giriş olarak `manifest.json`, ardından her otel için bir fiyat dosyası
(`hotels/hotelonly/EDF----<tenant>-<hotel>.xml`, EDF 5.1.6) ve bir allotment dosyası
(`hotels/hotelonly/allotment/EDF----<tenant>-<hotel>.xml`, HotelAllotmentRoot 1.012) içeren bir zip.
Kimlik manifestten veya `BasicData`'dan alınır, asla dosya adından alınmaz (kodlar `-`
içerebilir). Her dosyanın sha256 sağlama toplamı ve uzunluğu manifestte yer alır; bir paket ya tamamen
uygulanır ya hiç uygulanmaz.

`objects` içinde bir giriş ve `removed` içinde bir giriş:

```json
{"path": "hotels/hotelonly/EDF----TEST-TENANT-A-TEST-HOTEL-BASE.xml", "kind": "hotel", "hotel": "TEST-HOTEL-BASE", "seq": 3, "sha256": "9f2c…", "bytes": 2210, "source_rev": 4}
```

```json
{"kind": "hotel", "hotel": "TEST-HOTEL-ALTAKTION", "seq": 5, "reason": "withdrawn:variant_error"}
```

- **Tombstone'lar:** Geri çekilen oteller `removed` altında nedenleriyle yer alır
  (`withdrawn:deleted`, `withdrawn:variant_error`, `withdrawn:not_exportable`,
  `withdrawn:no_currency`, `withdrawn:not_in_universe`). Alıcı bunları kendi
  önbelleğinden siler. Bir `full` her zaman `removed: []` içerir – önbelleği tamamen değiştirir.
- **Allotment dosyasının pattern'i:** gece başına iki karakter (`PatternLength="2"`, MTS
  teslimatları gibi): `00`–`99` = bu anahtar için boş birimler, `**` = 99'dan fazla boş veya
  serbest satış, `SS` = satış durdurma, `RR` = talep üzerine; kapasitesiz bir gece `00`'dır.
  Bir allotment dosyası eksikse otel müsait değil kabul edilir. Oda başına yapı:
  `<Allotments RoomCode="DZ"><Allotment Start="JJJJ-MM-TT" End="JJJJ-MM-TT" Pattern="…"/>`;
  `Pattern`'in ilk iki karakteri `Start` gecesine, sonraki her çift bir sonraki geceye aittir.
- **Pattern ve `minFree`:** `SS`, `RR` ve `00` açıkça `minFree` `0` verir
  (rezerve edilemez); yalnızca `**` `minFree`'yi düşürmez ve `-1` şu anlama gelir: her gece `**`. Bir
  konaklamanın geceleri boyunca minimum, aynı anahtar için `/v1/price`'ın `availability.minFree` değeridir
  (Bölüm 3.3'teki tablo) – ikisi de gece başına aynı kaynaktan gelir.

### 10.2 Tam durum, değişiklikler, onay
<!-- de:b72e8777641d -->

Başlangıçta ve `409`/`410` sonrasında alıcı tam durumu çeker. Manifestten
`epoch` ve `to_seq` değerlerini not eder. Tam durum bir pakete sığmazsa (10.000'den fazla dosya,
açılmış halde 1 GiB'den fazla veya `max_bytes`'tan fazla), sayfalar halinde gelir, bkz. aşağıda „Sayfalar“:

```http
### export-voll
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{apiKey}}
```

```json
{
  "format": "tourapi-edf-feed/1",
  "tenant": "TEST-TENANT-A",
  "scope": "",
  "epoch": "{{exportEpoch}}",
  "type": "full",
  "from_seq": 0,
  "to_seq": "{{*}}",
  "more": false,
  "generated_at": "{{*}}",
  "rules": {"edf": "5.1.6", "allotment": "1.012", "spec": ""},
  "objects": "{{*}}",
  "removed": []
}
```

Her `200`/`204` yanıtının header'ları: `X-Export-Epoch`, `X-Export-Seq` (son
tam durum), `X-Export-From` (teslimatın başladığı durum), `X-Export-More`;
bir `changes` paketinde ve `more: true` olan bir `full` sayfasında ayrıca
`X-Export-Until` (zincir anahtarı, bkz. aşağıda). Bir sayfa bir durumun ortasında biterse (manifestte `to_after`),
`X-Export-Seq` ondan önceki durumu belirtir – bir `full`'ün ilk sayfalarında yani
`0`. `since`, uygulama ve onay için belirleyici olan manifestteki `to_seq`/`to_after`'dır. Bir
müşteri grubunun anahtarı kendi grubunun durumunu (`scope`), grubun fiyatlarıyla alır:

```http
### export-partner
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{rabattKey}}
```

Ardından döngü içinde değişiklikleri sorgular. `since`, son uygulanan paketin
`to_seq` değeridir; zaten güncel durumdaysa içeriksiz `204` gelir:

```http
### export-nichts-neu
GET {{baseUrl}}/v1/export/edf/changes?epoch={{exportEpoch}}&since={{exportSeq}}
X-Api-Key: {{apiKey}}
```

**Sayfalar (`max_bytes`):** `full` ve `changes` sayfa zincirleridir. Bir sayfada en fazla
10.000 dosya ve açılmış halde 1 GiB bulunur; `max_bytes=N` ile sunucu ayrıca en fazla `N` baytlık
sayfalar keser (sayfa başına en az bir dosya); `N` en az 65536'dır (64 KiB), daha küçüğü
`400 ERR_EXPORT_BAD_CURSOR`'dır. İlk sayfa (parametresiz `full` veya
`until` olmadan `changes`) zincirin hedefini (güncel durumu) belirler ve bunu
**zincir anahtarı** olarak `X-Export-Until` içinde verir (`full`'de yalnızca `more: true` ise): anahtara ve `epoch`'a
bağlı, opak, mühürlü bir karakter dizisi (`changes`'te `u1.…`, `full`'de `f1.…`).
Alıcı sonraki sayfaları `since=<to_seq>` veya
`since=<to_seq>:<to_after>` (manifest `to_after` taşıyorsa) **ve**
`until=<X-Export-Until>` (önceki sayfanın değeri, URL kodlu, değiştirilmeden) ile, `full`'de ayrıca
`epoch=<epoch>` (ilk sayfanın değeri) ile, `more` `false` olana kadar sorgular. Her sayfa anahtarı yeniden
verir; anahtar son sayfadan sonra 15 dakika, `full`'de yalnızca tam olarak sonraki sayfanın
durumu için geçerlidir. Kendi seçilmiş bir `until` (sayı), yabancı veya süresi dolmuş bir anahtar
`400 ERR_EXPORT_BAD_CURSOR`'dır – o zaman zinciri yeniden başlatın. Bir `full` sayfası asla
`removed` taşımaz; ilki `from_seq: 0`'da, sonraki her biri önceki sayfanın `to` değerinde başlar. Durum ancak
zincirin sonunda tutarlıdır: uygulama (değiştirme) orada yapılır, tur operatörü
arada değişiklik yapmaya devam etse bile – zincir tam olarak hedefinde biter.

**İptal ve tekrarlama:** Bir paket, `200` zaten gönderildikten sonra başarısız olursa (örneğin
tur operatöründe bir dosya bu arada eksik olduğu için), sunucu bağlantıyı keser. Alıcı
o zaman bir iletim hatası görür (`unexpected EOF`, bağlantı sıfırlandı), asla
düzgün biçimde sonlandırılmış, kısaltılmış bir paket görmez. Yine de her paketi uygulamadan önce
manifestine karşı kontrol **etmelidir**: `objects` içindeki her dosya mevcut, uzunluk (`bytes`) ve `sha256`
doğru, fazladan dosya yok. Bir akış okuyucusu (ör. Java'nın `ZipInputStream`'i), bir giriş sınırında biten
bir zip'i kendiliğinden eksik olarak tanımaz. Kısa süreliğine başarısız olan bir sonraki sayfayı
(iletim hatası, yarıda kesilmiş veya okunamayan paket, `5xx`), zinciri yeniden başlatmak yerine
aynı `epoch`/`since`/`until` ile tekrar sorgular (anahtar 15 dakika geçerlidir, `503` durumunda
`Retry-After` sonrasında) – yeni bir başlangıç döngü hakkını tüketir (`full`:
bir saat). Yalnızca `400` sonrasında zinciri yeniden başlatır, `409`/`410` sonrasında `full` çeker. Bir
zincir yarıda kalırsa eski durum geçerli kalır.

Uyguladıktan sonra alıcı durumu onaylar. Onay isteğe bağlıdır ve
teslimatta hiçbir şeyi değiştirmez; tur operatörü bundan alıcının hangi durumu işlediğini
görür:

```http
### export-quittung
POST {{baseUrl}}/v1/export/edf/ack
Content-Type: application/json
X-Api-Key: {{apiKey}}

{"epoch": "{{exportEpoch}}", "seq": {{exportSeq}}}
```

### 10.3 Hatalar ve döngü
<!-- de:e49c1625c8dc -->

| Durum | Kod | Ne zaman | Alıcının yapacağı |
|---|---|---|---|
| 400 | `ERR_EXPORT_BAD_CURSOR` | `epoch`/`since` eksik veya okunamıyor, `until` geçerli bir zincir anahtarı değil (yabancı, süresi dolmuş, sayı, `full`'de başka bir durum için), `max_bytes` ≥ 65536 bir sayı değil, `until` olmadan bir durumun ortasında sayfa, durum zincir hedefinin üzerinde, `full`'ün `epoch`/`since`/`until` olmadan sonraki sayfası | İsteği düzeltin veya zinciri yeniden başlatın |
| 403 | `ERR_EXPORT_NOT_ALLOWED` | Dışa aktarım yetkisi olmayan anahtar | Tur operatörüne sorun |
| 403 | `ERR_KEY_GROUP_INACTIVE` | Anahtarın müşteri grubu devre dışı (asla sessizce temel sözleşme) | Tur operatörüne sorun |
| 409 | `ERR_EXPORT_EPOCH` | `epoch` uymuyor (feed yeniden oluşturuldu) veya `since`/`until` ya da onayın `seq` değeri güncel teslimat durumunun **üzerinde** (son teslim edilen durumdan büyük; daha eski bir `since`'e izin verilir ve sonrasındaki her şeyi teslim eder) | `full` |
| 410 | `ERR_EXPORT_CURSOR_EXPIRED` | Durum saklama süresinden (14 gün) eski | `full` |
| 429 | `ERR_RATE_LIMITED` | Döngü aşıldı, bkz. aşağıda | `Retry-After` sonrasında |
| 503 | `ERR_EXPORT_NOT_READY` | Bu anahtar için teslimat henüz hiç oluşturulmadı (yeni anahtar, yeni grup) veya teslimat geçici olarak güncel değil (verilerin 2 dakikadan fazla gerisinde) | `Retry-After` sonrasında, eski durum geçerli kalır |

```http
### export-stand-unlesbar
GET {{baseUrl}}/v1/export/edf/changes?epoch={{exportEpoch}}&since=gestern
X-Api-Key: {{apiKey}}
```

```http
### export-fremder-stand
GET {{baseUrl}}/v1/export/edf/changes?epoch=01J00000000000000000000000&since=1
X-Api-Key: {{partnerKey}}
```

```json
{"errorCode": "ERR_EXPORT_EPOCH", "message": "epoch veraltet (Feed neu aufgebaut) oder Stand neuer als der aktuelle Lieferstand — full abrufen"}
```

```http
### export-ohne-recht
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{poolKey}}
```

```json
{"errorCode": "ERR_EXPORT_NOT_ALLOWED", "message": "dieser API-Key hat kein Export-Recht (EDF-Lieferung) — der Veranstalter-Admin schaltet es frei"}
```

**Anahtar başına döngü:** yeni bir `full` saatte en fazla bir kez, yeni bir `changes` zinciri
dakikada en fazla bir kez. Bir zincirin sonraki sayfaları (`full` veya `changes`,
zincir anahtarıyla) ve onayların kendi, cömert döngüsü vardır (saniyede 5, bir
seferde 50). Bir zincir başlangıcının hangi çağrısı döngü hakkını tüketir:

| Yanıt | döngü hakkını tüketir |
|---|---|
| `400 ERR_EXPORT_BAD_CURSOR`, `422 ERR_UNKNOWN_FIELD` (geçersiz istek, her işten önce kontrol edilir) | hayır |
| `503 ERR_EXPORT_NOT_READY` (teslimat henüz hiç oluşturulmadı veya geçici olarak güncel değil) | hayır – `Retry-After`'a uyan 429 almaz |
| `200` (yarıda kesilmiş bir indirme dahil), `204` („yeni bir şey yok“), `409 ERR_EXPORT_EPOCH` | evet |

Döngü kontrolü, `epoch` ve durum kontrolünden **önce** gelir. Eski
`epoch`'a sahip bir alıcı döngü içinde önce `429`'u, ancak onun `Retry-After` süresinden sonra `409`'u görür.
Örnek akış `changes` (ölçülmüş, döngü 1 dk):

| Zaman | İstek | Yanıt | Alıcının yapacağı |
|---|---|---|---|
| 0 sn | `changes?epoch=E&since=S&foo=1` (yazım hatası) | `422 ERR_UNKNOWN_FIELD` | düzeltin; döngü hakkı tüketilmedi |
| 0 sn | `changes?epoch=E&since=S` | `204` | Döngü hakkı tüketildi; sonraki zincir en erken 60 sn sonra |
| 0 sn | `changes?epoch=E&since=S`, feed bu arada yeniden oluşturuldu (`E` eski) | `429 ERR_RATE_LIMITED`, `Retry-After: 60` | `Retry-After` süresini bekleyin |
| 60 sn | aynı çağrı | `409 ERR_EXPORT_EPOCH` | `full` çekin (kendi döngüsü, saatte 1) |

Bunun ötesinde anahtarın hız sınırı geçerlidir (Bölüm 8). Aynı saat içinde ikinci bir `full`:

```http
### export-zu-oft
GET {{baseUrl}}/v1/export/edf/full
X-Api-Key: {{apiKey}}
```

```json
{"errorCode": "ERR_RATE_LIMITED", "message": "full hoechstens einmal je 1 h je API-Key (danach changes)"}
```

### 10.4 Uygulama ve önbellekten hesaplama (referans alıcı)
<!-- de:dbf5ec93710d -->

TourAPI'nin tam olarak bu kuralları uygulayan ve her derlemede
gerçek API'ye karşı kontrol edilen bir referans alıcısı vardır: `edf-empfaenger` aracı (işletmeciden talep üzerine)
(`pull`, `apply`, `stand`, `rechne`, `reset`; anahtar yalnızca
`EDF_EMPFAENGER_KEY` ortam değişkeninden gelir). Teslimattan hesapladığı şey, test kümesinin her isteği için
aynı anahtarın `/v1/price` sonucuyla sent düzeyinde aynıdır – toplam fiyat, yolcu başına fiyat, seçilen oda,
`available` ve `minFree`, ret durumunda aynı kod; bu, geçersiz ve
birden fazla açıdan geçersiz istekler için de geçerlidir (hangi sınırın önce devreye girdiği). Referans alıcı ve
`make e2e-export` TourAPI'nin kaynak ağacında bulunur ve teslimatın parçası değildir: Siz
müşteri olarak anahtar ve erişim paketi alırsınız ve bu bölümün kurallarını kendi sisteminizde
uygularsınız – referans alıcı, bu kuralların işlediğinin kanıtıdır.

```
EDF_EMPFAENGER_KEY=… edf-empfaenger pull   --dir <bestand> --api <baseUrl> [--max-bytes N] [--ohne-ack]
edf-empfaenger stand  --dir <bestand>
edf-empfaenger rechne --dir <bestand> --anfragen anfragen.json [--heute JJJJ-MM-TT]
```

`anfragen.json` bir listedir; her istek için `hotel`, `room` (isteğe bağlı, yoksa = en uygun fiyatlı
müsait oda), `board`, `checkIn`, `checkOut` ve yolcuların yaşları `ages` olarak
(`/v1/price`'taki gibi `occupancy.travellers` değil), ör.
`[{"hotel": "TEST-HOTEL-BASE", "room": "DZ", "board": "RO", "checkIn": "2026-10-27",
"checkOut": "2026-10-29", "ages": [40, 38]}]`. Yanıt her istek için `anfrage` (indeks),
`room`, `currency`, `totalCents`, `perTravellerCents`, `availability` (`available`, `minFree`)
veya `errorCode` belirtir.

- **Ya hep ya hiç:** Paketi kontrol edin (dosya başına sha256 ve uzunluk), yeni durumu
  eskisinin yanında eksiksiz kurun, ardından atomik olarak geçiş yapın; ancak bundan sonra `epoch`
  ve `to_seq` kaydedilmiş sayılır. Sayfalı bir zincir (`full` gibi `changes` de) ancak
  zincirin sonunda (`more: false`) geçiş yapılır; yarıda kalırsa eski durum geçerli kalır. Bir
  `full` zinciri her zaman `from_seq: 0` ile başlar; bir `full` sonraki sayfası yalnızca
  açık zincire bağlanır, asla kaydedilmiş bir duruma bağlanmaz – `from_seq` değeri
  kaydedilmiş `to_seq`'e eşit olsa bile (bir `full` `removed` taşımaz, aksi halde silinen oteller
  yerinde kalırdı).
- **Asla geriye doğru değil:** Belirleyici olan zincirin hedefidir, yani son sayfanın `to_seq` değeri
  (`more: false`), ilk sayfanınki değil. Bir `full` zincirinin ilk sayfası çoğu zaman
  kaydedilmiş durumdan daha küçük bir `to_seq` taşır (örneğin `410` sonrasında: en eski dosyalar
  önce gelir) ve yine de bir geri adım değildir. Hedefi kaydedilmiş durumdan küçük olan,
  aynı `epoch`'a ait bir `full` zinciri veya daha eski bir `epoch`'a ait bir `full` (`epoch`
  zamana göre sıralı bir ULID'dir) reddedilir – gecikmeli teslim edilmiş eski bir paket
  önbelleği geri almaz. `changes` durumuna boşluksuz bağlanmalıdır
  (`from_seq` = kaydedilmiş `to_seq`). Başka bir kiracının veya kapsamın `full`'ü
  (karıştırılmış anahtar) da reddedilir, geriye doğru kontrolden önce kapsam hatası olarak.
- **Reddedilen `full`:** Eski durum geçerli kalır, önbellek onunla hesaplamaya devam eder, ancak
  artık yeni bir şey çekmez. Bu kasıtlı olarak yüksek sesle olur – asla sessizce geri sıçramaz. Karıştırılmış
  anahtar söz konusu değilse ve teslimat gerçekten daha eski bir `epoch` üzerindeyse (örneğin saati
  geri kalan bir makinede yapılan bir geri yüklemeden sonra: yeni `epoch` ULID'si o zaman daha küçüktür) veya
  daha küçük bir `to_seq` üzerindeyse, durumu bilinçli olarak sıfırlayın (referans alıcı:
  `edf-empfaenger reset`) ve yeni bir `full` çekin. Şüphe durumunda önce işletmeciye
  danışın.
- **Çekme sırasında okuma:** Bir çekme uzun sürebilir (büyük zincir, `429` beklemesi). Önbellekten
  fiyatlar bu sırada eski durumla, geçişten sonra ise yeni durumla çalışmaya devam eder;
  eski durumu ancak artık kimse onu okumuyorsa silin. Referans alıcı
  stok başına bir yazıcıya (`pull`, `apply`, `reset`) ve istenen sayıda okuyucuya (`stand`,
  `rechne`) izin verir. `429` için yanıt başına en fazla `--max-warten`, çekme başına
  toplam en fazla `--max-warten-gesamt` bekler, sonra iptal eder (durum değişmez).
- **Yeniden başlamak yerine tekrarlama:** Kısa süreli hata veren bir sonraki sayfayı
  referans alıcı aynı parametrelerle üç kereye kadar çeker (10.2'deki kural);
  zincir başlangıcını tekrarlamaz.
- **Önbellekten fiyat:** Beyan edilen kurallarla EDF 5.1.6, ayrıca API'nin kuralları:
  aynı istek sınırları (Ek, aynı hata kodları; referans tarihi = Europe/Berlin'de
  bugün); oda ile bu oda kodunun en uygun fiyatlı teklifi, oda olmadan
  en uygun fiyatlı **müsait** oda, hiçbiri müsait değilse fiyatlandırılabilir en uygun fiyatlı oda (
  Bölüm 3.1'deki gibi); pansiyon tipini sunmayan bir oda fiyatlandırılmaz.
- **Beyan edilen kurallar:** Her otel dosyası `SellingData` içinde çocuk sıralamasını belirtir
  (`ChildrenAgeOrder`, `Descending` = önce en büyük çocuk: „1. çocuk“ kimdir, hangi çocuk
  boş bir tam ücret yerini doldurur), `FullPayerDoesNotAffectBoardCharges/PersonType=C` (tam ücret
  yerindeki bir çocuk tam temel fiyatı, pansiyon tipini ise çocuk tarifesiyle öder)
  ve `Rounding Mode="Commercial" DecimalPlace="2" Scope="Person"` (yolcu başına bir yuvarlama,
  Bölüm 1.4). Şemanın serbestlik bıraktığı yerlerde TourAPI şöyle hesaplar – önbellek de
  aynısını yapmalıdır, aksi halde sapar: `MinCount`/`MaxCount` çocukları kişi türü başına
  ilk çocuktan itibaren, yetişkinleri tam ücret ödeyenlerden sonraki ilk yetişkinden itibaren sayar; bir bebek bir
  kişi koşulunda („3 kişiden itibaren“) yalnızca oda onu doluluğa sayıyorsa sayılır
  (`Infants/@ApplyToOccupancy` `Min`, `Max` veya `Yes`); eksik bir `ExtraMaxApply` 1 anlamına gelir; `Operator="AND"` ile
  birden fazla tarih penceresi kesişim olarak geçerlidir; bir ücretsiz gecenin yanındaki yüzde ek ücret ve
  indirimleri yalnızca bağışlanmayan gecelere uygulanır; kişi başına sabit bir tutar (`PS`,
  `PN`) obje fiyatında da kişi başına etki eder; oda kalemleri (obje fiyatı, doldurulmamış
  tam ücret yeri) en büyük yolcuya aittir ve onunla birlikte yuvarlanır.
- **Pattern'den müsaitlik:** Odanın `[checkIn, checkOut)` geceleri boyunca minimum,
  10.1'deki gibi; dosyanın belirtmediği bir gece kapalıdır (asla „hiçbir şey teslim edilmediği için
  açık“ değil). `/v1/price`'ın `configured` alanının önbellekte karşılığı yoktur:
  kontenjanı olmayan bir oda orada `00` olarak yer alır.
- **Rezervasyondan önce canlı:** Önbellek API'nin bir durum gerisinde olabilir; önbellekte
  açık, bu arada kapatılmış veya dolmuş bir geceyi `/v1/book` reddeder
  (`ERR_STOP_SALE`, `ERR_SOLD_OUT`).

---

## 11. Sandbox: test anahtarları ve test rezervasyonları
<!-- de:f669d7e8b4cf -->

Bir **test anahtarı** (`tk_test_…`) bir canlı anahtarın ikizidir: aynı tur operatörü, aynı
müşteri grubu, aynı yetkiler (dışa aktarım yetkisi, ileride arama profili) — çalışma zamanında canlı anahtardan
devralınır. Canlı anahtarla **aynı verileri** okur (oteller, fiyatlar, hedefler,
müsaitlik, EDF teslimatı), ancak **rezervasyonlar sandbox'a düşer**: asla
tur operatörüne değil, kontenjan yok, dışa aktarım yok, konsolun rezervasyon listesi yok. Böylece
rezervasyon, rezervasyon bilgisi, iptal, idempotency ve tekrarlama mantığı gerçek tekliflerle test edilebilir.

- Test anahtarını tur operatörü konsolda canlı anahtar için oluşturur (anahtar satırındaki „Test-Key“
  düğmesi) ve kendi erişim paketiyle iletir. 30 gün geçerlidir (en fazla
  90), erişim başına en fazla 5 aktif test anahtarı.
- Canlı anahtar askıya alınmışsa veya test anahtarının süresi dolmuşsa: `401 ERR_UNAUTHORIZED`. Müşteri grubu
  devre dışı: canlıdaki gibi `403 ERR_KEY_GROUP_INACTIVE`. Canlı anahtar yenilendikten sonra test
  anahtarı yeni canlı anahtarla çalışmaya devam eder; asla canlı anahtarından daha geç sona ermez.
- Tur operatörü izin verdiyse test anahtarını iş ortağı portalında kendiniz de verebilirsiniz
  (7, 30 veya 90 gün).
- Mod için belirleyici olan anahtarın öneki değil, tur operatöründeki eşleştirmedir.

### 11.1 İşaretleme ve karıştırmaya karşı koruma
<!-- de:3b299d9dd0db -->

Kabul edilen bir anahtara verilen her yanıt **`X-TourAPI-Mode: test`** veya
**`live`** header'ını taşır (header adlarını HTTP'de olağan şekilde büyük/küçük harf ayrımı
yapmadan karşılaştırın: sunucu `X-Tourapi-Mode` yazar). Bir test anahtarının rezervasyon, rezervasyon bilgisi ve iptal yanıtları ayrıca
**`"sandbox": true`** alanını taşır, referans **`SB-`** ile başlar (canlıda `TA-`). Test anahtarıyla okuma
canlı anahtarla aynı yanıtı verir:

```http
### sandbox-preis
POST {{baseUrl}}/v1/price
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Require-Mode: test

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "board": "RO",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "occupancy": {"travellers": [{"age": 40}, {"age": 38}]}
}
```

Test ortamları (CI, ajanlar, geliştirici bilgisayarları) **`X-TourAPI-Require-Mode:
test`** header'ını gönderir. Bu durumda bir canlı anahtar gelirse API **her** isteği reddeder ve hiçbir şey yürütmez – böylece
yanlışlıkla kullanılan bir canlı anahtar rezervasyon yapamaz (kontrol istemcide değil,
sunucudadır). `X-TourAPI-Require-Mode: live` tersine bir canlı anahtar gerektirir; başka bir değer
`400 ERR_BAD_REQUEST`'tir.

```http
### sandbox-live-key-in-testumgebung
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{apiKey}}
X-TourAPI-Require-Mode: test

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0003"
}
```

```json
{"errorCode": "ERR_MODE_MISMATCH", "message": "X-TourAPI-Require-Mode: test verlangt, der API-Key ist ein live-Key — nichts ausgefuehrt"}
```

### 11.2 Test rezervasyonu, rezervasyon bilgisi, iptal
<!-- de:56e30d172e7a -->

Test anahtarıyla `/v1/book`, canlıdakiyle **aynı kontrollerden aynı sırayla** geçer
(alanlar, tekrar, otel, referans tarihi ve sınırlar, satış kuralları, `priceCheck`,
müsaitlik) ve aynı hata kodlarını verir. Müsaitlik kontrol edilir, ancak
tüketilmez:

```http
### sandbox-buchen
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Require-Mode: test

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0001",
  "reference": "TEST-4711",
  "leadPaxName": "Test Person",
  "priceCheck": {
    "board": "RO",
    "currency": "EUR",
    "occupancy": {"travellers": [{"age": 40}, {"age": 38}]},
    "expectedCents": 18000,
    "tolerancePercent": 0
  }
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{sandboxRef}}", "correlationId": "{{*}}", "sandbox": true}
```

```http
### sandbox-buchung-lesen
GET {{baseUrl}}/v1/booking?ref={{sandboxRef}}
X-Api-Key: {{testKey}}
```

```json
{"reference": "{{sandboxRef}}", "customerReference": "TEST-4711", "hotel": "TEST-HOTEL-BASE", "room": "DZ",
 "checkIn": "{{D2}}", "checkOut": "{{D4}}", "quantity": 1, "status": "confirmed",
 "bookedAt": "{{*}}", "updatedAt": "{{*}}", "totalCents": 18000, "currency": "EUR", "board": "RO", "sandbox": true}
```

Test ve canlı dünya ayrıdır: Bir test anahtarı yalnızca test rezervasyonlarını görür ve iptal eder, bir
canlı anahtar yalnızca gerçek olanları. Aynı `idemKey` her iki dünyada birbirinden bağımsız olarak rezervasyon yapar.

```http
### sandbox-live-key-sieht-testbuchung-nicht
GET {{baseUrl}}/v1/booking?ref={{sandboxRef}}
X-Api-Key: {{apiKey}}
```

```json
{"errorCode": "ERR_BOOKING_NOT_FOUND", "message": "keine Buchung zu '{{sandboxRef}}'"}
```

```http
### sandbox-stornieren
POST {{baseUrl}}/v1/cancel
Content-Type: application/json
X-Api-Key: {{testKey}}

{"idemKey": "BEISPIEL-0001"}
```

```json
{"released": true, "alreadyReleased": false, "sandbox": true}
```

- Test rezervasyonları oluşturulmalarından 30 gün sonra silinir. Erişim başına en fazla 5.000 açık (iptal
  edilmemiş) test rezervasyonu, üzerinde `422 ERR_SANDBOX_LIMIT`.
- Bir test anahtarının çağrılarını (metot, yol, durum, `errorCode`, süre, istek ve yanıt)
  TourAPI 7 gün boyunca kaydeder, böylece tur operatörü hata ayıklamada yardımcı olabilir.
  Bu nedenle: test rezervasyonlarında **test isimleri** kullanın.
- Test anahtarıyla EDF teslimatı: çağrılar (`full`, `changes`) ve onaylar (`ack`) yalnızca
  test çağrılarının bu kaydında yer alır, tur operatörünün **teslimat kaydında asla** yer almaz – teslimatın
  health durumunda alım veya işleme olarak sayılmazlar.

### 11.3 Senaryolar: hata durumlarını zorlama
<!-- de:3ec5e343e1de -->

**`X-TourAPI-Sandbox-Scenario`** header'ı ile bir test anahtarı bir hata durumunu zorlar –
tekrarlama mantığını test etmek için (Bölüm 8 ve 9). Bir canlı anahtarla, bilinmeyen bir adla
veya senaryonun etkisiz olduğu bir endpoint'te: `422 ERR_SCENARIO_NOT_ALLOWED`.

| Senaryo | Endpoint'ler | Etki |
|---|---|---|
| `booking_busy` | `/v1/book`, `/v1/cancel` | `idemKey` ve endpoint başına ilk deneme (rezervasyon ve iptal ayrı sayılır): `Retry-After: 1` ile `503 ERR_BOOKING_BUSY`, hiçbir şey rezerve veya iptal edilmez; **aynı** `idemKey` ile tekrar başarılı olur |
| `price_drift` | `priceCheck` ile `/v1/book` | `idemKey` başına ilk deneme: `409 ERR_PRICE_DRIFT`; fiyatın kendisi değişmez (`message` güncel ve beklenen fiyatı verir, ikisi aynıdır); yeni fiyatı alın, yeniden rezervasyon yapın |
| `sold_out` | `/v1/book` | her zaman `422 ERR_SOLD_OUT` (diğer tüm kontrollerden sonra) |
| `rate_limited` | tümü | test anahtarı ve endpoint başına ilk istek: `Retry-After: 1` ile `429 ERR_RATE_LIMITED` |
| `search_busy` | `/v1/search` | test anahtarı başına ilk istek: `Retry-After: 1` ile `429 ERR_SEARCH_BUSY` |
| `price_timeout` | `/v1/price`, `/v1/prices` | test anahtarı ve endpoint başına ilk istek: `503 ERR_PRICE_TIMEOUT` |

„İlk istek“ 10 dakika geçerlidir; bu süre içindeki her tekrar normal şekilde işlenir.
Bellek API'nin her düğümünde ayrı tutulur.

```http
### sandbox-szenario-busy
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Sandbox-Scenario: booking_busy

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0002"
}
```

```json
{"errorCode": "ERR_BOOKING_BUSY", "message": "Buchung/Storno kam wegen gleichzeitiger Vorgaenge am selben Hotel nicht durch; nichts geaendert — mit demselben idemKey wiederholen (Sandbox-Szenario booking_busy)"}
```

```http
### sandbox-szenario-busy-wiederholt
POST {{baseUrl}}/v1/book
Content-Type: application/json
X-Api-Key: {{testKey}}
X-TourAPI-Sandbox-Scenario: booking_busy

{
  "hotel": "TEST-HOTEL-BASE",
  "room": "DZ",
  "checkIn": "{{D2}}",
  "checkOut": "{{D4}}",
  "quantity": 1,
  "idemKey": "BEISPIEL-0002"
}
```

```json
{"booked": true, "alreadyBooked": false, "reference": "{{*}}", "correlationId": "{{*}}", "sandbox": true}
```

```http
### sandbox-szenario-live-key
GET {{baseUrl}}/v1/destinations
X-Api-Key: {{apiKey}}
X-TourAPI-Sandbox-Scenario: rate_limited
```

```json
{"errorCode": "ERR_SCENARIO_NOT_ALLOWED", "message": "X-TourAPI-Sandbox-Scenario: nur mit einem Test-Key (Sandbox)"}
```

### 11.4 Test anahtarlarının sınırları
<!-- de:2703b1987e23 -->

Bir erişimin tüm test anahtarları **tek bir** kotayı paylaşır: saniyede 20 istek, burst 40 (bunun yanında
canlı anahtarın sınırı etkilenmez). Bir tur operatörünün test anahtarları en fazla 2
eşzamanlı arama kullanır. EDF teslimatının döngüsü (`full` 1/saat, yeni `changes` zinciri 1/dk)
test anahtarı başına değil, erişim başına geçerlidir.

### 11.5 Sandbox'ın kanıtlamadıkları
<!-- de:59e8a96897d8 -->

- **Kontenjan ve son oda için yarış:** Test rezervasyonları hiçbir şey tüketmez. Bir
  test rezervasyonu, canlıda başkasının daha hızlı olduğu yerde başarılı olabilir.
- Rezervasyondan sonra **tur operatöründeki süreçler** (onay, değişiklik, fatura).
- **Canlı yük altındaki davranış** (ayrı, daha küçük kota, bkz. 11.4).

### 11.6 Yapay zeka ajanları: portaldaki MCP sunucusu
<!-- de:47144e35bd7f -->

Portal, `/mcp` adresinde bir MCP sunucusu sunar (Model Context Protocol, “Streamable HTTP”
aktarımı, durumsuz, yalnızca araçlar). Claude Code, Codex veya Antigravity gibi bir yapay zeka
ajanı, HTTP kodu yazmak yerine API'yi bu araçlar üzerinden çağırır. Her araç, MCP isteğinin
anahtarıyla (`X-Api-Key` veya `Authorization: Bearer`) ve `X-TourAPI-Require-Mode: test` ile
yapılan **tam olarak bir** `/v1` çağrısıdır – davranış, sınırlar, yalıtım ve hata kodları
API'ninkilerdir. **Yalnızca test anahtarları:** canlı anahtar `ERR_MODE_MISMATCH` (403) alır,
hiçbir şey yürütülmez. Portal hiçbir anahtarı saklamaz.

| Araç | Çağrı | Argümanlar |
|---|---|---|
| `list_destinations` | `GET /v1/destinations` | yok |
| `search_hotels` | `POST /v1/search` | istek gövdesi; sayfalar `cursor` ile |
| `price_offer` | `POST /v1/price` | istek gövdesi |
| `price_all_rooms` | `POST /v1/prices` | istek gövdesi |
| `sandbox_book` | `POST /v1/book` | istek gövdesi, `idemKey` zorunlu |
| `get_booking` | `GET /v1/booking` | `ref` |
| `sandbox_cancel` | `POST /v1/cancel` | istek gövdesi |
| `get_limits` | `GET /v1/limits` | yok |
| `open_search` | `POST /v1/search/open` | istek gövdesi |
| `open_search_dates` | `POST /v1/search/open/dates` | istek gövdesi |
| `hotel_details` | `GET /v1/content/hotels/{code}` | `code`, `lang` |

İstek gövdesi olan araçlar argümanlarını değiştirmeden JSON gövdesi olarak API'ye iletir (şema =
uç noktanın OpenAPI tanımı). Diğerleri yalnızca listelenen argümanları alır; başka bir argüman
`ERR_UNKNOWN_FIELD`, geçersiz bir değer `ERR_BAD_REQUEST` verir – API çağrısı yapılmadan.

**`hotel_details` yalnızca içerik yetkisiyle.** `tools/list`, isteğin anahtarıyla `GET /v1/limits`
sorgular ve `hotel_details` aracını yalnızca `content.allowed: true` ise listeler (içerik API'siyle
aynı kural, 4c); anahtar yoksa listede yer almaz. Yine de çağrılırsa API `ERR_CONTENT_NOT_ALLOWED`
(403) döndürür. API bu kontrolde anahtarı reddederse (ör. canlı anahtar için `ERR_MODE_MISMATCH`)
veya yanıt vermezse `tools/list`, `message` içinde hata kodu ya da nedenle bir JSON-RPC hatasıdır
(`-32000`) – araç listesi yoktur.

**`hotel_details` ve `lang`.** Tur operatörü istenen bir içerik dilini sunmuyorsa araç
`ERR_LANGUAGE_NOT_OFFERED` hatasını iletmez: içerik dillerini okur (`GET /v1/content/catalog`,
`languages`) ve istenen dilleri sunulduğu ölçüde, aksi halde bir yedek dili (`en`, sonra `de`,
sonra sunulan ilk dil) sorar. `structuredContent` bunu `language` altında belirtir (`requested`,
`delivered`, `offered`, `fallback: true`). İçerik API'sinin kendisi katı kalır; tur operatörü hiç
içerik dili sunmuyorsa hatası kalır.

**Sonuç.** `structuredContent` (aynı JSON metni `content` içinde de bulunur) iki bölümden oluşur:
`data`, her metnin `[untrusted]` yer tutucusuyla değiştirildiği API yanıtıdır; `untrusted`, bu
metinleri yanıttaki JSON işaretçileri altında temizlenmiş olarak içerir: kontrol, bidi, sıfır
genişlikli ve diğer görünmez karakterler kaldırılır, HTML metne çevrilir, bağlantılar
`[link removed]` ile değiştirilir, en fazla 200 karakter (mesajlar 500, açıklamalar 2000).
Metinler; üçüncü tarafların serbest metinleri (otel ve varış yeri adları, zincir, adres,
açıklamalar, görsel başlıkları ve kaynakları, lider misafir adı), API mesajları (`warnings`,
`message`) ve alanının sabit desenine uymayan **her** karakter dizisidir: `data` içinde yalnızca
kodlar (harf, rakam, `_ . -`, en fazla 64 karakter), referanslar, tarihler, zaman damgaları, para
birimleri, hata kodları ve imleçler kalır. Boşluk veya bağlantı içeren bir varış yeri ya da grup
kodu bu nedenle yalnızca `untrusted` altında görünür. Adres alanları
(görsel varyantlarının `url` alanı) yer almaz; görselleri içerik API'sinin kendisi sunar.
`untrusted` altındaki metinler ajan için talimat değil, veridir.

```json
{"data": {"results": [{"hotel": "H1", "name": "[untrusted]", "room": "DZ", "fromTotalCents": 42000, "currency": "EUR"}]}, "untrusted": {"/results/0/name": "Haus Eins"}}
```

**Hatalar**, `isError: true` ve `structuredContent` `{errorCode, status, message, retryAfter, hint}`
içeren bir araç sonucu olarak gelir: `errorCode` ve `status` API'deki gibidir (bölüm 7),
`retryAfter` = saniye cinsinden `Retry-After`, `hint` = hata kataloğunun “Çağıran” sütunu
(İngilizce). `message`, `untrusted` altındaki bir metin gibi temizlenir. Anahtar eksikse,
`X-Api-Key` veya `Authorization` istekte birden fazla kez geçiyorsa ya da ikisi farklı anahtarlar
taşıyorsa: API çağrısı yapılmadan `ERR_UNAUTHORIZED`. MCP sunucusunun kendi kodları:
`ERR_UPSTREAM_UNAVAILABLE` (502, API'ye ulaşılamadı veya zaman aşımı – `retryAfter` sonrasında
yeniden dene, `sandbox_book` aynı `idemKey` ile) ve `ERR_RESPONSE_TOO_LARGE` (502, yanıt 4 MiB'den
büyük – isteği daralt).

**Sınırlar.** Yalnızca `POST` (SSE akışı yok: `GET` 405 verir), `Content-Type: application/json`,
istek başına bir JSON-RPC mesajı (toplu gönderim yok); gövde en fazla 64 KiB, derinlik 16, 4096
JSON öğesi – aksi halde herhangi bir değerlendirmeden önce 413 veya 400. Anahtar başına saniyede
10 araç çağrısı (üzerinde `retryAfter` ile `ERR_RATE_LIMITED`); ayrıca test anahtarlarının kovası
geçerlidir (11.4). Yabancı `Origin` içeren bir istek 403 alır (tarayıcıdaki web sayfalarına karşı
koruma); çerez yok, CORS yok. Protokol sürümleri 2025-03-26, 2025-06-18 ve 2025-11-25.

**Kurulum.** Test anahtarını `TOURAPI_API_KEY` ortam değişkeni olarak ayarlayın (kendi kodunuzdakiyle aynı; tek
değişken yeterli); yapılandırmaya yalnızca ona yapılan referans girer. Claude Code, projede `.mcp.json` dosyası:

```json
{"mcpServers": {"tourapi": {"type": "http", "url": "https://portal.example.de/mcp", "headers": {"X-Api-Key": "${TOURAPI_API_KEY}"}}}}
```

Codex (anahtar, ortam değişkeninden bearer token olarak gönderilir):

```sh
codex mcp add tourapi --url https://portal.example.de/mcp --bearer-token-env-var TOURAPI_API_KEY
```

Antigravity `headers` içine ortam değişkeni doldurmaz; `mcp-remote` köprüsü anahtarı ortamdan
okur (`mcp_config.json` içindeki girdi; anahtarı denetlenmemiş bir sürüm almasın diye köprü
sürümü sabittir):

```json
{"mcpServers": {"tourapi": {"command": "npx", "args": ["-y", "mcp-remote@0.14.3", "https://portal.example.de/mcp", "--header", "X-Api-Key:${TOURAPI_API_KEY}"]}}}
```

Portaldaki “Yapay zeka ajanları” sayfası aynı girdileri portalınızın adresiyle gösterir.

---

## 12. Otel içerikleri: `/v1/content/*`
<!-- de:2da15ec87ce4 -->

Content API, tur operatörünün otel içeriklerini alıcıların web siteleri ve katalogları için
sunar: ana veriler (tesis türü, zincir, adres), konum (koordinatlar), kategoriler, bilgiler,
metinler, görseller ve olanaklar. Fiyat ve rezervasyonun yanında ayrı bir yoldur: içerikler
hiçbir fiyatı ve müsaitliği değiştirmez.

| Rota | Amaç |
|---|---|
| `GET /v1/content/hotels` | Anahtarın otellerinin dizini, `code`'a göre artan, sayfalı |
| `GET /v1/content/hotels/{code}` | Bir otelin içeriği (`ETag`, `If-None-Match` → `304`) |
| `GET /v1/content/changes?since=…` | Bir durumdan bu yana değişiklikler, kaydedildikleri sırayla |
| `GET /v1/content/catalog` | Etiketli kataloglar (tesis türleri, kategori ölçekleri, metin ve görsel türleri, olanaklar) |

### 12.1 Erişim ve kapsam
<!-- de:d890a03a2b08 -->

- Content API yalnızca **ikisi de** geçerliyse yanıt verir: platform işletmecisi içerikleri tur
  operatörü için etkinleştirmiştir ve anahtar **içerik yetkisini** taşır (tur operatörü
  yöneticisi API erişimi altında verir, varsayılan kapalı). Aksi halde
  `403 ERR_CONTENT_NOT_ALLOWED`. Bir test anahtarı canlı anahtarının yetkisine sahiptir ve aynı
  içerikleri okur.
- Platform işletmecisinin test ortamı olarak yürüttüğü bir tur operatörüne yalnızca bunun için
  kurulan kurulum hizmet verir. Diğer her yerde etkin değil sayılır: aynı yanıt
  `403 ERR_CONTENT_NOT_ALLOWED`.
- Bir anahtarın hangi otelleri gördüğünü `/v1/destinations`'daki gibi anahtar belirler (1.1):
  müşteri grubu olmadan ve fiyat grubuyla tur operatörünün tüm otelleri, kontenjan grubuyla
  yalnızca tahsisi olan oteller – müsaitlikten bağımsız. Diğer her otel
  `404 ERR_HOTEL_NOT_FOUND`'dur.
- Yalnızca tur operatörünün **içerik dillerindeki** görünür metinler ve görünür, tamamen
  işlenmiş görseller sunulur. Otelin **iletişim bilgileri** (telefon, e-posta, web) Content
  API'de yoktur, boş alan olarak bile.
- Görseller herkese açık, tahmin edilemeyen adreslerde bulunur
  (`<base>/m/<key>/<size>.jpg`, anahtarsız alınabilir, 1 gün önbelleğe alınabilir).
  Her görselin yanında `credit` vardır; `attributionRequired: true` ise atıf görselin yanında
  gösterilmelidir.
- Anahtarlar tarayıcı koduna ait değildir (1.1): web sitesi Content API'yi sunucuda okur,
  tarayıcıya yalnızca görsel adresleri gider.

### 12.2 Dizin
<!-- de:5353c4c32522 -->

```http
### inhalt-verzeichnis
GET {{baseUrl}}/v1/content/hotels?pageSize=2
X-Api-Key: {{apiKey}}
```

```json
{
  "hotels": [
    {"code": "TEST-HOTEL-ALTAKTION", "name": "TEST Altaktion Teneriffa", "destination": "TFS", "contentVersion": 0, "websiteReady": false, "missing": ["general_text", "geo", "category", "images", "amenities"]},
    {
      "code": "TEST-HOTEL-BASE",
      "name": "TEST Basis Palma",
      "destination": "PMI",
      "contentVersion": "{{*}}",
      "updatedAt": "{{*}}",
      "category": {"kind": "official", "scheme": "stars", "value": 4, "superior": true},
      "geo": {"lat": 39.565, "lon": 2.627, "precision": "address"},
      "websiteReady": false,
      "missing": ["images", "amenities"]
    }
  ],
  "nextCursor": "{{*}}",
  "feedToken": "{{*}}",
  "scopeHash": "{{*}}"
}
```

- `pageSize` 1–1000 (varsayılan 500). Sonraki sayfa `cursor=<nextCursor>` ile alınır;
  `nextCursor` yoksa dizin eksiksizdir.
- `contentVersion` bir otel için sunulan içeriğin her değişikliğini sayar; `0` = henüz içerik
  yok (o zaman `updatedAt` yoktur). `category` resmi ülke kategorisidir, `geo` konum –
  ikisi de yalnızca bakımı yapılmışsa. `websiteReady` ve `missing` bir otelin içeriğindeki
  gibi (12.3).
- `feedToken`, `/v1/content/changes`'in devam ettiği durumdur. Bir dizin geçişinin tüm
  sayfaları aynı durumu taşır (ilk sayfanınkini).
- `scopeHash`, anahtarın otel kümesi değişir değişmez değişir (yeni veya silinmiş otel,
  tahsis eklendi veya kaldırıldı). Bu akışta yer almaz: o zaman dizini yeniden alın.
- `cursor`, `feedToken` ve `next` opaktır ve anahtara bağlıdır: başka bir anahtarla,
  değiştirilmiş olarak veya sunucunun anahtar değişikliğinden sonra `422 ERR_BAD_CURSOR`.

```http
### inhalt-verzeichnis-weiter
GET {{baseUrl}}/v1/content/hotels?pageSize=2&cursor={{inhaltCursor}}
X-Api-Key: {{apiKey}}
```

```json
{
  "hotels": [
    {"code": "TEST-HOTEL-CLOSED", "name": "TEST Geschlossen Mahon", "destination": "MAH", "contentVersion": 0, "websiteReady": false, "missing": ["general_text", "geo", "category", "images", "amenities"]},
    {
      "code": "TEST-HOTEL-DISCOUNT",
      "name": "TEST Rabatt Palma",
      "destination": "PMI",
      "contentVersion": "{{*}}",
      "updatedAt": "{{*}}",
      "category": {"kind": "official", "scheme": "stars", "value": 4},
      "geo": {"lat": 39.5696, "lon": 2.6502, "precision": "locality"},
      "websiteReady": false,
      "missing": ["general_text", "images", "amenities"]
    }
  ],
  "nextCursor": "{{*}}",
  "feedToken": "{{*}}",
  "scopeHash": "{{*}}"
}
```

### 12.3 Bir otelin içeriği
<!-- de:f3187966234f -->

```http
### inhalt-hotel
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=de,tr
X-Api-Key: {{apiKey}}
```

```json
{
  "code": "TEST-HOTEL-BASE",
  "name": "TEST Basis Palma",
  "destination": "PMI",
  "contentVersion": "{{*}}",
  "updatedAt": "{{*}}",
  "accommodationType": "HOTEL",
  "categories": [{"kind": "official", "scheme": "stars", "value": 4, "superior": true}],
  "address": {"street": "Passeig Marítim 12", "postalCode": "07014", "city": "Palma", "region": "Mallorca", "country": "ES"},
  "geo": {"lat": 39.565, "lon": 2.627, "precision": "address"},
  "facts": {"rooms": 120, "checkInFrom": "14:00", "checkOutUntil": "11:00"},
  "texts": [
    {"type": "GENERAL", "lang": "de", "html": "<p>TEST-Hotel am Strand mit <b>Pool</b> und Garten.</p>\n<p>TEST-Lage: ruhige Bucht.</p>", "updatedAt": "{{*}}"},
    {"type": "GENERAL", "lang": "de", "fallbackFrom": "tr", "html": "<p>TEST-Hotel am Strand mit <b>Pool</b> und Garten.</p>\n<p>TEST-Lage: ruhige Bucht.</p>", "updatedAt": "{{*}}"}
  ],
  "media": [],
  "amenities": [
    {"code": "WIFI_ROOM", "available": true, "charge": "included"},
    {"code": "SPA", "available": false},
    {"code": "DIST_AIRPORT", "available": true, "distanceM": 12000, "ref": "PMI"}
  ],
  "websiteReady": false,
  "missing": ["images", "amenities"]
}
```

- **Diller:** `lang` olmadan tur operatörünün tüm içerik dillerindeki tüm metinler gelir.
  `lang` ile (1–5 dil, ISO 639-1 küçük harf, virgülle ayrılmış) her metin türü ve istenen dil
  için tam olarak bir kayıt gelir. Metin bu dilde yoksa tur operatörünün varsayılan dili,
  ardından İngilizce devreye girer; kayıt o zaman istenen dili `fallbackFrom` olarak taşır
  (burada: Türkçe metin yok, Almanca gelir). Orada da yoksa kayıt eksik kalır. Görsellerin
  başlıkları (`titles`) ve alternatif metinleri (`alts`) aynı kurala uyar. Tur operatörünün
  sunmadığı bir dil: `422 ERR_LANGUAGE_NOT_OFFERED` (etkin diller `warnings` içindedir).
- `html` yalnızca özniteliksiz `p`, `br`, `b`, `strong`, `i`, `em`, `ul`, `ol`, `li` içerir.
  `machineTranslated: true` bir makine çevirisini işaretler.
- `categories`: `kind` `official` (ülke kategorisi) veya `operator` (tur operatörünün
  derecelendirmesi), `scheme` `stars` veya `keys`, `value` yarım adımlarla 1–5; "4 Superior"
  `value: 4, superior: true`'dur, asla 4,5 değil.
- `geo.precision`: `address`, `street`, `locality` veya `unknown`.
- `media` gösterim sırasıyla, `order: 1` ana görseldir. Her görsel için oluşturulan
  genişliklerle `variants` (`w320` ile `w2048` arası, asla orijinalden geniş değil), her biri
  genişlik, yükseklik, bayt ve `url` ile; `focus` (yüzde olarak x, y) kendi kırpmalarınız için
  görselin en önemli noktasıdır. `id`, görsel otelde kaldığı sürece sabittir.
- `amenities`: katalogdan kodlu olanaklar (12.5). `available: false` açıkça mevcut değil
  demektir; eksik bir olanak bilinmiyordur. Olanağa göre `count`, `distanceM`, `areaM2`,
  `ref` (mesafe için havalimanı kodu) ve ücretli olabilen olanaklarda `charge` (`included`,
  `extra`, `unknown`) ile.
- `name`, `destination` ve `giataCode` otelin sözleşmesinden gelir. Bunlardan biri değişirse
  otel akışta yer alır (12.4, `reason: contract`) ve bundan sonra detay ile dizin yeni değeri
  döndürür.
- `websiteReady` ve `missing`: "web sitesine hazır" olgunluk düzeyi – tur operatörünün bir web
  sitesi için yeterli içerik girip girmediği. Yalnızca bir göstergedir; ne satışı ne de sunumu
  değiştirir. `missing` eksik ölçütleri sabit sırayla listeler, boş = web sitesine hazır:
  `general_text` (operatörün varsayılan dilinde genel metin), `geo` (konum), `category`
  (kategori veya tesis türü), `images` (ana görsel dahil en az 5 sunulan görsel), `amenities`
  (açıkça "mevcut değil" dahil en az 10 girilmiş özellik). Değerlendirme yalnızca içerikle
  değişir (yeni `contentVersion`); operatörün konsolu aynısını gösterir. `missing` içindeki
  bilinmeyen değerleri yok sayın (ölçüt eklenebilir). Dizinde de her iki alan bulunur.

Her yanıt bir `ETag` taşır. İçerik, dil seçimi ve gösterimle birlikte değişir.
`If-None-Match` ile hiçbir şey değişmediği sürece gövdesiz `304` gelir:

```http
### inhalt-hotel-unveraendert
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=de,tr
X-Api-Key: {{apiKey}}
If-None-Match: {{inhaltEtag}}
```

`lang` olmadan tüm içerik dilleri (burada `en` makine çevirisi olarak):

```http
### inhalt-hotel-alle-sprachen
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE
X-Api-Key: {{apiKey}}
```

```json
{
  "code": "TEST-HOTEL-BASE",
  "name": "TEST Basis Palma",
  "destination": "PMI",
  "contentVersion": "{{*}}",
  "updatedAt": "{{*}}",
  "accommodationType": "HOTEL",
  "categories": "{{*}}",
  "address": "{{*}}",
  "geo": "{{*}}",
  "facts": "{{*}}",
  "texts": [
    {"type": "GENERAL", "lang": "de", "html": "{{*}}", "updatedAt": "{{*}}"},
    {"type": "GENERAL", "lang": "en", "html": "<p>TEST hotel on the beach with a <b>pool</b> and garden.</p>\n<p>TEST location: quiet bay.</p>", "machineTranslated": true, "updatedAt": "{{*}}"}
  ],
  "media": [],
  "amenities": "{{*}}",
  "websiteReady": false,
  "missing": ["images", "amenities"]
}
```

### 12.4 Değişiklikler
<!-- de:3a01f66e9c6b -->

```http
### inhalt-aenderungen
GET {{baseUrl}}/v1/content/changes?since={{feedToken}}
X-Api-Key: {{apiKey}}
```

```json
{"changes": [], "next": "{{*}}", "more": false, "scopeHash": "{{*}}"}
```

- `since`, dizinin `feedToken`'ı veya önceki yanıtın `next` değeridir (zorunlu, aksi halde
  `400 ERR_BAD_REQUEST`). `pageSize` 1–1000 (varsayılan 500).
- Her kayıtta `code`, `change` (`upsert` = içerik değişti, yeniden alın; `removed` = otel
  silindi), `upsert` için güncel `contentVersion` ve isteğe bağlı `reason` (`languages` = tur
  operatörünün içerik dilleri değişti, `media_ready` = bir görsel tamamen işlendi, `contract` =
  `name`, `destination` veya `giataCode` sözleşmeyle değişti, `removed` için `deleted`).
  Bilinmeyen değerleri yok sayın.
- Değişikliklerin kaydedildiği sırayla, boşluksuz; bir otel her sayfada en fazla bir kez yer
  alır (son durumuyla). `more: true` = hemen `next` ile okumaya devam edin.
- Yalnızca anahtarın otelleri. Otel kümesinin değişmesi akışta değil, `scopeHash`'te yer alır.
- Akış 30 gün geriye gider. Daha eski bir durum: `410 ERR_CONTENT_CURSOR_EXPIRED` – o zaman
  dizini yeniden alın ve onun `feedToken`'ı ile devam edin.

### 12.5 Katalog
<!-- de:84b72293a015 -->

```http
### inhalt-katalog
GET {{baseUrl}}/v1/content/catalog?lang=de
X-Api-Key: {{apiKey}}
```

```json
{
  "version": "2026.1",
  "languages": ["de", "en", "tr"],
  "defaultLanguage": "de",
  "labelLanguages": ["de"],
  "accommodationTypes": "{{*}}",
  "categorySchemes": [{"code": "sterne", "sort": 1, "labels": {"de": "Sterne"}}, {"code": "schluessel", "sort": 2, "labels": {"de": "Schlüssel"}}],
  "textTypes": "{{*}}",
  "mediaTypes": "{{*}}",
  "amenityGroups": "{{*}}",
  "amenities": "{{*}}"
}
```

- `languages` tur operatörünün içerik dilleridir, `defaultLanguage` varsayılan dilidir.
  `lang` etiket dillerini seçer (`de`, `en`, `tr`; `lang` olmadan tümü).
- Her olanak için `group`, `valueType` (`flag`, `anzahl`, `meter`, `flaeche_m2`,
  `meter_mit_bezug`), `unit` ve `chargeable`. Bir kod asla yeniden yorumlanmaz; `locked: true`
  kaldırıldı demektir (okunabilir kalır). `version` her yeni katalogla değişir; katalog bir
  `ETag` taşır.

### 12.6 Alıcılar için eşitleme
<!-- de:c81e8058f698 -->

1. **İlk yükleme:** dizini sayfa sayfa alın, `feedToken` ve `scopeHash`'i saklayın, her otel
   için içeriği alın ve `ETag`'i kaydedin.
2. **Sürekli** (aşağı yukarı birkaç dakikada bir): `changes?since=<token>` (ilk çağrıda
   `feedToken`, sonrasında en son saklanan `next`),
   değişen otelleri yeniden alın, `removed` olanları silin, `next`'i saklayın. `scopeHash`
   değişirse: hemen 3. adım.
3. **Günlük ve `410` durumunda:** dizini yeniden alın ve kendi verilerinizle eşitleyin (eksik
   otelleri silin, yenilerini alın, `contentVersion`'ı karşılaştırın).
4. İçerikleri her zaman `If-None-Match` ile alın.

Erişim ve kapsam hataları:

```http
### inhalt-ohne-recht
GET {{baseUrl}}/v1/content/hotels
X-Api-Key: {{poolKey}}
```

```json
{"errorCode": "ERR_CONTENT_NOT_ALLOWED", "message": "dieser API-Key hat kein Inhalts-Recht — der Veranstalter-Admin schaltet es unter API-Zugang frei"}
```

```http
### inhalt-nicht-im-verzeichnis
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-DISCOUNT
X-Api-Key: {{partnerKey}}
```

```json
{"errorCode": "ERR_HOTEL_NOT_FOUND", "message": "Hotel 'TEST-HOTEL-DISCOUNT' nicht im Verzeichnis dieses API-Keys"}
```

```http
### inhalt-sprache-nicht-angeboten
GET {{baseUrl}}/v1/content/hotels/TEST-HOTEL-BASE?lang=fr
X-Api-Key: {{apiKey}}
```

```json
{"errorCode": "ERR_LANGUAGE_NOT_OFFERED", "message": "lang: 'fr' ist keine Inhaltssprache dieses Veranstalters (ISO 639-1, klein)", "warnings": ["aktive Inhaltssprachen: de, en, tr (Standard de)"]}
```

```http
### inhalt-stand-fremder-key
GET {{baseUrl}}/v1/content/changes?since={{feedToken}}
X-Api-Key: {{partnerKey}}
```

```json
{"errorCode": "ERR_BAD_CURSOR", "message": "since: unlesbar, von einem anderen API-Key oder nicht von diesem Server — den Wert der Vorseite unveraendert zurueckgeben, sonst neu beginnen"}
```

---

## 13. Planlananlar ve sınırlar
<!-- de:646fc31e9488 -->

**Planlananlar:** `/v1` içinde yalnızca eklemeli değişiklikler yapılır (bölüm 1.9); her
değişiklik tarihiyle birlikte değişiklik günlüğünde yer alır. Şu anda mevcut bir entegrasyonun
uyarlanmasını gerektiren duyurulmuş bir değişiklik yoktur.

**Sınırlar:** Sayısal değerler (boyutlar, hızlar, zaman sınırları) "Bir bakışta sınırlar"
ekindedir; sandbox'ın kapsamadıkları 11.5'tedir.

---

## Ek: Bir bakışta sınırlar
<!-- de:9fb3c803996a -->

| Ne | Değer |
|---|---|
| İstek başına body | 1 MiB |
| Konaklama başına gece | 1–30 |
| Giriş | referans tarihinden referans tarihi + 732 güne kadar |
| İstek başına yolcu, yaş | 1–20, 0–120 |
| Rezervasyon başına oda (`quantity`) | 1–1.000.000 |
| `idemKey`, `reference` | 128 karakter (daha uzunu: `422 ERR_VALIDATION`) |
| `leadPaxName` | 255 karakter (daha uzunu: `422 ERR_VALIDATION`) |
| `metadata.correlationId` | 64 karakter (daha uzunu: `422 ERR_VALIDATION`) |
| Anahtar başına hız | 200/sn, burst 400 |
| Erişim başına test anahtarları | en fazla 5, 90 güne kadar geçerli; toplam 20/sn, burst 40; tur operatörü başına 2 eşzamanlı arama |
| Test rezervasyonları | erişim başına 5.000 açık, oluşturulduktan 30 gün sonra silinir; test çağrılarının kaydı 7 gün |
| Arama süre sınırı | 10 sn |
| Arama sayfası | varsayılan 50, en fazla 100 otel |
| Açık arama | anahtar başına arama profiline göre (pencere, süreler, hedefler, sayfa, zaman bütçesi, istek hızı; tarih matrisi: pencere ve hücreler), `GET /v1/limits` ile alınabilir; belirtilmezse sayfa 20; cursor 15 dk |
| Bağlantı | 15 sn okuma, 15 sn yazma |
| Anahtar başına EDF teslimatı | `full` 1/saat, yeni `changes` zinciri 1/dk, sonraki sayfalar/onaylar 5/sn (burst 50); paket yanıtları 10 dakikaya kadar yazabilir |
| Content API | dizin ve akış sayfası 1–1000 (varsayılan 500), `lang` 1–5 dil, akış 30 gün |
