İçeriğe geç

Katalog

Katalog uçları, size açılan villaları ve onları tanımlayan sözlükleri (özellik, lokasyon, villa tipi) verir. Hepsi GET isteğidir, aynı kimlikle çağrılır ve ETag taşır.

Bu sayfadaki örnekler şu ortam değişkenlerini kullanır (ayrıntı Başlarken):

Ortam
export TRIMANGO_BASE_URL="https://…/api/public/v1" # size verilen taban adres, sonunda / yok
export TRIMANGO_CLIENT_ID="…"
export TRIMANGO_CLIENT_SECRET="…"

Node örneklerinde ortak bir yardımcı var:

trimango.js
const BASE = process.env.TRIMANGO_BASE_URL
const AUTH =
'Basic ' + Buffer.from(`${process.env.TRIMANGO_CLIENT_ID}:${process.env.TRIMANGO_CLIENT_SECRET}`).toString('base64')
export async function api(path, { etag } = {}) {
const res = await fetch(BASE + path, {
headers: { Authorization: AUTH, Accept: 'application/json', ...(etag ? { 'If-None-Match': etag } : {}) },
})
if (res.status === 304) return { notModified: true, etag }
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
return { data: await res.json(), etag: res.headers.get('etag') }
}

Katalogda yalnız iki durumdaki villa görünür:

status Anlamı
FOR_SALE Satışta.
CLOSED Satışa geçici olarak kapalı. Listede ve detayda görünür, ama kapalı olduğu dönemde teklif ve talep alınmaz. Ayrıntı Müsaitlik.

Taslak, yayından kaldırılmış ya da arşivlenmiş villa hiçbir uçta görünmez; detayı 404 döner. Kendi kataloğunuzda bir villanın 404 dönmesini “artık satılmıyor” olarak yorumlayın.

GET /villas süzülmüş, sıralanmış ve sayfalanmış villa kartlarını döner.

Kalkan ve Kaş'ta, en az 6 kişilik, havuzlu villalar
curl -s -u "$TRIMANGO_CLIENT_ID:$TRIMANGO_CLIENT_SECRET" \
"$TRIMANGO_BASE_URL/villas?location=kalkan,kas&guests=6&features=ozel-havuz&sort=price_asc&page=1&page_size=48&facets=false"
Node
const { data } = await api('/villas?location=kalkan,kas&guests=6&features=ozel-havuz&sort=price_asc&page_size=48&facets=false')
console.log(data.total, data.items.map((v) => v.code))
Parametre Kural
location İl, ilçe ya da belde adresi (slug), virgülle birden çok; biri tutması yeter. Adresler GET /locations’dan. Aynı adlı ilçe birden çok ilde varsa adres il önekiyle gelir (mugla-merkez); sorguda mugla/merkez de kabul edilir. Belde takma adı da çalışır (Hisarönü → hisaronu); o zaman yanıttaki resolved_location eşlendiği asıl adresi verir.
region Bölge kimlikleri (uuid, virgülle). GET /locations yanıtındaki regions[].id.
type Villa tipi adresleri, virgülle; biri tutması yeter. GET /types/counts’tan.
features Özellik kimlikleri ya da kalıcı adresleri (slug), virgülle; hepsi olan villalar. Bilinmeyen değer hiçbir villayla eşleşmez. GET /features’tan.
guests Kapasite ile ek kişi hakkının toplamı en az bu kadar olan villalar.
bedrooms, bathrooms En az yatak odası, en az banyo.
min_price, max_price Başlayan gecelik fiyatın TL karşılığına göre bant.
max_sea_distance_m Denize ya da plaja en çok bu kadar metre. Mesafesi girilmemiş villa çıkar.
pet_friendly, breakfast_included true: evcil hayvan kabul eden villalar; kahvaltı fiyata dahil olan villalar.
deal Fırsat türleri (LAST_MINUTE, EARLY_BOOKING, CAMPAIGN, GIFT_NIGHT), virgülle; en az biri bugün yayında olan villalar.
featured true: yalnız öne çıkarılan villalar.
q Villa adı ya da kodu. Büyük/küçük harf ve Türkçe harf farkı gözetilmez; bütün kelimeler geçmeli.
check_in, check_out, adults, children, available_only Tarihli liste: her kartta uygunluk ve toplam fiyat. Müsaitlik sayfasında.
month Ay modu (YYYY-MM): kartta o aydaki boş haftalar. Müsaitlik sayfasında.

Bütün parametrelerin tam listesi ve sınırları API başvurusunda.

sort şu değerlerden biridir:

Değer Sıra
recommended (varsayılan) Önce öne çıkarılan villalar, sonra fırsatlı villalar, sonra son güncellenen.
price_asc, price_desc Başlayan fiyatın TL karşılığı; fiyatı olmayan villalar sonda.
newest Son eklenen önce.
capacity_desc, capacity_asc Kapasite; eşitlikte başlayan fiyat.
rating_desc Misafir puanı; eşitlikte yorum sayısı, yorumsuz villalar sonda.

page 1’den başlar; page_size 1–48, varsayılan 24. Yanıttaki total süzgece uyan bütün villaların sayısıdır, sayfa sayısını Math.ceil(total / page_size) ile bulun.

GET /villas → 200
{
"items": [
{
"id": "01990c1e-7a3b-7c2d-9e4f-5a6b7c8d9e0f",
"code": "ABC-1042",
"slug": "villa-zeytin",
"slug_en": "villa-olive",
"title": "Villa Zeytin",
"status": "FOR_SALE",
"location": {
"slug": "kalkan",
"name": "Kalkan",
"district": { "slug": "kas", "name": "Kaş" },
"city": { "slug": "antalya", "name": "Antalya" },
"region": null
},
"capacity": 6,
"bedrooms": 3,
"bathrooms": 3,
"cover_url": "https://…",
"photos": ["https://…", "https://…"],
"types": [{ "slug": "balayi-villalari", "name": "Balayı villaları" }],
"from_price": { "nightly": 450, "currency": "EUR", "nightly_try": 16425 },
"deals": [],
"offers": [],
"lat": 36.2654,
"lng": 29.4141,
"sea_distance_m": 800,
"stay": null,
"month_free_weeks": null,
"month_min_nightly_try": null,
"month_weeks": null,
"rating": 4.8,
"review_count": 12,
"has_pool": true,
"pet_friendly": false,
"breakfast_included": false
// … is_featured, is_sort_priority, ribbon_text, ribbon_text_2, why
}
],
"facets": null,
"nearby_free": [],
"resolved_location": null,
"total": 37,
"page": 1,
"page_size": 48,
"occupied_count": null,
"personalized": false
}

Karttaki önemli alanlar:

Alan Anlamı
id Villanın kalıcı kimliği (uuid). Kendi veritabanınızda anahtar olarak bunu kullanın.
code Villa kodu, Trimango genelinde tekil. Biçimini varsaymayın (ABC-1042 gibi önekli ya da eski villalarda VR-<n>). Kod nadiren değişebilir; yanıttaki değer esastır.
slug, slug_en Villanın Türkçe ve İngilizce adresi. Değişebilir; anahtar olarak kullanmayın.
location Belde (slug, name), ilçesi, ili ve varsa bölgesi.
capacity Standart kişi sayısı. Ek kişi hakkı detayda extra_person.max_extra.
cover_url, photos Kapak ve ilk 5 fotoğrafın küçük boyu. Büyük boylar detayda; bkz. Fotoğraflar.
from_price Başlayan fiyat: bugünden sonra biten sezonların en düşük gecelik fiyatı, villanın para biriminde (nightly, currency) ve TL karşılığı (nightly_try, kur yoksa null). Sezonu olmayan villada null. Gerçek konaklama fiyatı için Fiyat ve teklif.
deals, offers Villayı kapsayan, bugün yayındaki fırsatların türü ve ayrıntısı.
rating, review_count Yayındaki misafir yorumlarının ortalaması (1–5) ve sayısı; yorum yoksa rating null.
stay, month_* Yalnız tarihli listede ve ay modunda dolu; diğer durumlarda null.

GET /villas/{slug} villanın bütün içeriğini, takvimini ve sezon fiyatlarını döner. Yoldaki değer üç biçimde olabilir:

  • Türkçe adres (slug): villa-zeytin
  • İngilizce adres (slug_en): villa-olive
  • Villa kodu: ABC-1042

Kod eşleşmesi kesindir; bu yüzden eşitlemede villayı koduyla açmanızı öneririz. Kodu değişen bir villa eski koduyla bir süre daha açılır, yanıttaki code yeni koddur: kaydınızı güncelleyin.

Villa detayı
curl -s -u "$TRIMANGO_CLIENT_ID:$TRIMANGO_CLIENT_SECRET" \
"$TRIMANGO_BASE_URL/villas/ABC-1042?calendar_months=12"
Node
const { data: villa } = await api(`/villas/${encodeURIComponent('ABC-1042')}?calendar_months=12`)

calendar_months (1–24, varsayılan 18) takvimin bugünden kaç ay ileriye verileceğidir.

GET /villas/ABC-1042 → 200 (kısaltılmış)
{
"id": "01990c1e-7a3b-7c2d-9e4f-5a6b7c8d9e0f",
"code": "ABC-1042",
"slug": "villa-zeytin",
"slug_en": "villa-olive",
"title": "Villa Zeytin",
"status": "FOR_SALE",
"closed_until": null,
"location": { "slug": "kalkan", "name": "Kalkan", "district": { "slug": "kas", "name": "Kaş" }, "city": { "slug": "antalya", "name": "Antalya" }, "region": null },
"capacity": 6,
"bedrooms": 3,
"bathrooms": 3,
"short_description": "Kalkan'a yürüme mesafesinde, deniz manzaralı …",
"content_html": "<p>…</p>",
"photos": [
{
"url": "https://…",
"thumb_url": "https://…",
"width": 1600,
"height": 1067,
"blurhash": "LKO2?U%2Tw=w]~RBVZRi};RPxuwH",
"category": "POOL",
"alt_text": "Havuz ve teras"
}
],
"video_url": null,
"lat": 36.2654,
"lng": 29.4141,
"size_m2": 180,
"views": [{ "code": "SEA", "name": "Deniz" }],
"privacy": "DETACHED",
"rooms": [
{
"type": "Yatak odası",
"kind": "BEDROOM",
"name": null,
"floor": 1,
"en_suite": true,
"beds": [{ "code": "DOUBLE", "name": "Çift kişilik", "count": 1 }]
}
],
"features": [
{
"id": "01990a00-1111-7000-8000-000000000001",
"name": "Özel havuz",
"slug": "ozel-havuz",
"icon": "pool",
"category": "havuz",
"category_name": "Havuz",
"kind": "FEATURE",
"note": null
}
],
"pools": [
{ "type": "Açık havuz", "width_cm": 400, "length_cm": 800, "depth_cm": 150, "heatable": true, "fenced": false, "sun_hours": 8, "fee": 1500, "fee_unit": "DAY", "note": null }
],
"house_rules": [
{ "code": "PETS", "name": "Evcil hayvan", "icon": null, "allowed": false, "note": null }
],
"check_in_time": "16:00",
"check_out_time": "10:00",
"stay_rules": [{ "from": null, "to": null, "min_nights": 5, "max_nights": null, "check_in_days": [] }],
"distances": [{ "label": "Plaj", "distance_m": 800, "type": "BEACH", "note": null }],
"sea_distance_m": 800,
"calendar": { "from": "2026-10-27", "to": "2027-10-27", "busy": [{ "start": "2027-06-12", "end": "2027-06-19" }] },
"currency": "EUR",
"extra_person": { "max_extra": 2, "adult_per_night": 40, "child_per_night": 20 },
"canonical": null,
"document_number": "12345"
// … trail, types, from_price, seasons, fees, offers, deposit_rate, deposit_only, damage_deposit,
// instant_booking, cancellation_note, heating, internet_mbps, parking_spaces, year_built …
}

Detayı başlıklara göre okuyun:

Konu Alanlar
Metin title, short_description (düz metin), content_html (HTML).
Fotoğraf ve video photos[], cover_url, video_url. Bkz. Fotoğraflar.
Kapasite ve odalar capacity, bedrooms, bathrooms, extra_person (en çok ek kişi ve gecelik ek kişi ücreti), rooms[] (oda türü kind: BEDROOM, BATHROOM, LIVING, KITCHEN, OTHER; yataklar beds[]).
Özellikler features[]. kind: FEATURE özellik, HIGHLIGHT öne çıkan özellik, INCLUDED fiyata dahil, EXCLUDED fiyata dahil değil. slug kalıcıdır, ad değişse de değişmez.
Havuz ve ısıtma pools[] (ölçü, ısıtılabilir mi, ısıtma ücreti fee ve birimi fee_unit: DAY ya da STAY), heating[].
Ev kuralları house_rules[]. code sabit koddur (PETS, SMOKING, PARTY, EVENTS, CHILDREN, QUIET_HOURS, VISITORS); kodu olmayan kural null döner, adıyla gösterin. allowed izinli mi.
Konaklama kuralları stay_rules[]: dönem (from–to, son gece dahil; ikisi de null ise her zaman), min_nights, max_nights, giriş günleri check_in_days (ISO: 1 pazartesi … 7 pazar; boşsa her gün). check_in_time, check_out_time.
Konum lat, lng, location, trail (il › ilçe › belde), distances[] (yer türü type: BEACH, CENTER, MARKET, AIRPORT …), sea_distance_m.
Yapı size_m2, land_m2, floor_count, single_level, wheelchair_accessible, year_built, year_renovated, views[], privacy (DETACHED müstakil, IN_COMPLEX site içinde, SHARED_GARDEN ortak bahçe), internet_mbps, parking_spaces, car_to_door, steps_to_entrance.
Takvim calendar: bkz. Müsaitlik.
Fiyat currency, seasons[], fees[], extra_person, offers[], deposit_rate, damage_deposit. Konaklama fiyatını kendiniz hesaplamayın: Fiyat ve teklif.
Belge document_number: Kültür ve Turizm Bakanlığı Turizm İşletmesi Belgesi numarası; girilmemişse null.

Değer verilmemiş alanlar null ya da boş dizi döner; alanlar yanıttan düşmez.

Kataloğu kendi veritabanınızda tutuyorsanız başlangıç noktası GET /villas/summaries’dir. Sayfasızdır: size açık bütün villaları tek yanıtta, koda göre sıralı ve hafif bir özetle döner.

Bütün villaların özeti
curl -s -u "$TRIMANGO_CLIENT_ID:$TRIMANGO_CLIENT_SECRET" \
"$TRIMANGO_BASE_URL/villas/summaries"
GET /villas/summaries → 200
[
{
"id": "01990c1e-7a3b-7c2d-9e4f-5a6b7c8d9e0f",
"code": "ABC-1042",
"slug": "villa-zeytin",
"slug_en": "villa-olive",
"title": "Villa Zeytin",
"location": { "slug": "kalkan", "name": "Kalkan", "district": { "slug": "kas", "name": "Kaş" }, "city": { "slug": "antalya", "name": "Antalya" }, "region": null },
"capacity": 6,
"bedrooms": 3,
"bathrooms": 3,
"types": [{ "slug": "balayi-villalari", "name": "Balayı villaları" }],
"cover_url": "https://…",
"from_price": { "nightly": 450, "currency": "EUR", "nightly_try": 16425 },
"updated_at": "2026-10-21T08:14:03Z",
"rating": 4.8,
"review_count": 12,
"canonical_url": null
}
// …
]

Belirli villaları istemek için ids verin (kimlik ya da kod, virgülle, en çok 100): /villas/summaries?ids=ABC-1042,ABC-1043.

  1. Tam eşitleme (ilk kurulumda ve günde bir kez):
    1. GET /villas/summaries ile güncel villa listesini alın.
    2. Listede olup sizde olmayan villaları ekleyin; sizde olup listede olmayanları satıştan kaldırın.
    3. Her villa için GET /villas/{code} ile detayı okuyun. Önceki yanıtın ETag’ini If-None-Match ile gönderin; değişmemişse 304 döner ve gövde gelmez.
    4. Sözlükleri (/features, /locations, /types/counts) yenileyin.
  2. Gün içinde: webhook olayı gelince yalnız etkilenen villaları yeniden okuyun.
Node: tam eşitleme
import { api } from './trimango.js'
export async function fullSync(db) {
const { data: summaries } = await api('/villas/summaries')
const liveIds = new Set(summaries.map((s) => s.id))
for (const old of await db.villaIds()) {
if (!liveIds.has(old)) await db.unpublish(old) // artık size açık değil
}
// Aynı anda birkaç istek yeter; dakikada 1.200 istek sınırı istemcinin bütün çağrılarını kapsar.
const queue = [...summaries]
const workers = Array.from({ length: 4 }, async () => {
for (let s = queue.shift(); s; s = queue.shift()) {
const saved = await db.villa(s.id)
try {
const r = await api(`/villas/${encodeURIComponent(s.code)}`, { etag: saved?.etag })
if (!r.notModified) await db.saveVilla(s.id, r.data, r.etag)
} catch (e) {
if (String(e.message).startsWith('404')) await db.unpublish(s.id)
else throw e
}
}
})
await Promise.all(workers)
}

Süzgeçlerde kullandığınız adresler ve kimlikler bu üç uçtan gelir. Seyrek değişirler; değiştiklerinde reference webhook olayı gelir.

GET /features: özellikler, kategori sırasıyla.

GET /features → 200
[
{
"id": "01990a00-1111-7000-8000-000000000001",
"slug": "ozel-havuz",
"name": "Özel havuz",
"icon": "pool",
"category": "havuz",
"category_name": "Havuz",
"filterable": true
}
// …
]

slug kalıcıdır ve GET /villas?features= kimlik yerine kabul eder. filterable: true olan özellikler süzgeç için seçilmiş olanlardır. Kendi sisteminizdeki özellik listesine eşlerken slug’ı kullanın.

GET /locations: il › ilçe › belde ağacı ve bölgeler, villa sayılarıyla. Yalnız villası olan lokasyonlar döner.

GET /locations → 200
{
"cities": [
{
"slug": "antalya",
"name": "Antalya",
"villa_count": 42,
"districts": [
{
"slug": "kas",
"name": "Kaş",
"villa_count": 30,
"locations": [
{ "slug": "kalkan", "name": "Kalkan", "villa_count": 21, "is_popular": true, "lat": 36.2654, "lng": 29.4141, "region": null }
// …
]
}
]
}
],
"regions": [
{ "id": "01990b00-2222-7000-8000-000000000001", "name": "Likya", "image_url": null, "villa_count": 30, "locations": ["kalkan", "kas-merkez"] }
]
}
  • Üç düzeyin her birinin slug’ı GET /villas?location= süzgecinde çalışır.
  • Aynı adlı ilçe birden çok ilde varsa ilçe adresi il önekiyle gelir (mugla-merkez, aydin-merkez); çakışmayan ilçeler öneksizdir. Bu adresleri yanıttan aldığınız gibi saklayın.
  • Bölgeler (regions) birkaç beldeyi bir araya toplar; süzgeçte region=<id>.

GET /types/counts: villa tipleri, villa sayılarıyla.

GET /types/counts → 200
[
{
"slug": "balayi-villalari",
"name": "Balayı villaları",
"icon": null,
"short_description": null,
"group": null,
"villa_count": 9,
"show_in_menu": true,
"is_family": false,
"is_special_day": true
}
// …
]

slug’ı GET /villas?type= süzgecinde kullanın. Villa kartındaki ve detaydaki types[] aynı adresleri taşır.

  • content_html HTML’dir. Kendi arayüzünüzde gösterecekseniz izin verilen etiketleri süzerek (sanitize) çizin.
  • short_description düz metindir.
  • Katalog metinleri bugün Türkçe döner.
  • photos[] gösterim sırasıyla gelir; ilk fotoğraf kapaktır (cover_url). Bir villada yüzü aşkın fotoğraf olabilir. url büyük boy (en çok 1600×1600, oran korunur), thumb_url küçük boy (480×320, kırpılmış). Kartlardaki cover_url ve photos küçük boydur.
  • width ve height en-boy oranı için, blurhash yüklenirken gösterilecek bulanık önizleme için, category (EXTERIOR, POOL, BEDROOM …) ve alt_text erişilebilirlik ve gruplama içindir.
  • Adresler mutlak ve imzalıdır. Adresin herhangi bir parçasını (boyut, yol) değiştirmeyin; imza bozulur ve görsel açılmaz.
  • Dönen biçim isteğin Accept başlığına göre seçilir (WebP, AVIF ya da JPEG). Fotoğrafı sunucunuza indiriyorsanız biçimi Content-Type’tan okuyun.
  • Fotoğraflar değişince villa olayı photos kapsamıyla gelir; o villanın detayını yeniden okuyun.

Detaydaki canonical ve özetteki canonical_url, villanın asıl sayfasının başka bir sitede olduğunu gösterir:

  • null: başka bir asıl sayfa bildirilmemiş.
  • Dolu: canonical.url villanın Türkçe asıl sayfası, canonical.url_en İngilizcesi, canonical.tenant_name o sayfanın sahibi firmanın marka adı. Özetteki canonical_url, canonical.url ile aynıdır.

Trimango, kataloğunuzu etkileyen bir değişiklik olduğunda tanımladığınız adrese kısa ve imzalı bir olay gönderir. Olay veriyi taşımaz, yalnız neyin değiştiğini söyler; güncel veriyi yine API’den okursunuz. Kurulum ve imza doğrulaması Webhook sayfasında.

type scopes Kataloğunuzda ne yapılır
villa detail villas[] içindeki villaların detayını yeniden okuyun.
villa photos Aynı; fotoğrafları güncelleyin.
villa status Detayı yeniden okuyun. 404 dönerse villayı satıştan kaldırın; CLOSED dönerse kapalı olarak işaretleyin.
availability calendar Takvimi yenileyin: Müsaitlik.
price prices Villaların sezon fiyatlarını ve başlayan fiyatını yenileyin.
price deals + all_villas Fırsatlar değişti; villas boştur, bütün villaların fırsat ve fiyat bilgisini yenileyin.
reference locations, dictionaries Sözlükleri (/locations, /features, /types/counts) yeniden okuyun. Villalarda sakladığınız adlar da değişmiş olabilir.
reference currency Döviz kuru değişti; sakladığınız *_try tutarları eskidi.
test — Hiçbir şey yapmayın, 2xx dönün.

Villa adresi değiştiyse olaydaki villada previous_slug (ve İngilizce için previous_slug_en) gelir. Villaları id ile sakladığınız sürece yapmanız gereken yalnız yeni slug’ı kaydetmektir.

Node: olay geldikten sonra (imza doğrulandıktan sonra)
export async function onTrimangoEvent(event, db) {
if (event.type === 'test') return
if (event.type === 'reference') return refreshDictionaries(db)
if (event.scopes.includes('all_villas')) return fullSync(db)
for (const v of event.villas) {
const saved = await db.villa(v.id)
const key = saved?.code ?? v.slug
try {
const r = await api(`/villas/${encodeURIComponent(key)}`)
await db.saveVilla(v.id, r.data, r.etag)
} catch (e) {
if (String(e.message).startsWith('404')) await db.unpublish(v.id)
else throw e
}
}
}

Webhook bir şekilde kaçarsa (sunucunuz kapalıydı, olay beş denemede de ulaşmadı) günlük tam eşitleme farkı kapatır.