Webhook
Katalogu, müsaitliği ve fiyatları kendi sisteminizde saklıyorsanız Trimango’da bir şey değiştiğinde haberiniz olmalı. Webhook bunun içindir: Trimango, değişiklikten birkaç saniye sonra sunucunuzdaki bir 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
Bölüm başlığı “Kurulum”- Sunucunuzda
https://ile başlayan,POSTkabul eden bir adres açın (ör.https://api.ornek.com/trimango/webhook). - Adresi Trimango ekibine iletin. Ekip adresi API istemcinize tanımlar.
- Ekip size bir imza anahtarı (
whsec_…) verir. Anahtar yalnız bir kez, güvenli bir kanaldan iletilir; Trimango’da şifreli durur ve bir daha gösterilmez. Sunucunuzda gizli bir ortam değişkeninde saklayın (ör.TRIMANGO_WEBHOOK_SECRET). - Ekipten bir test olayı isteyin.
type: "test"olan bir olay gelir; doğrulamanız çalışıyorsa 2xx dönün.
- Her istemcinin kendi adresi ve anahtarı vardır: test ve canlı istemcileriniz ayrı ayrı tanımlanır.
- Anahtarı kaybederseniz ya da sızdığını düşünürseniz Trimango ekibinden yenisini isteyin. Yeni anahtar üretildiği anda eskisiyle imza biter; yeni anahtarı sunucunuza hemen yazın.
- Trimango ekibi son 30 günün gönderimlerini (zaman, tür, HTTP kodu, deneme sayısı, son hata) görür ve tek bir gönderimi elle yeniden yollayabilir. Olay gelmediğini düşünüyorsanız ekibe yazın.
- İstemci iptal edilir ya da webhook kapatılırsa bekleyen olaylar da iptal edilir.
POST /trimango/webhook HTTP/1.1Content-Type: application/json; charset=utf-8User-Agent: Trimango-Webhook/1X-Trimango-Event-Id: 0f8e3c1e-5d2a-4f6b-9a71-2c3d4e5f6a7bX-Trimango-Event-Type: availabilityX-Trimango-Timestamp: 1760518800X-Trimango-Signature: sha256=5d41402abc4b2a76b9719d911017c592…{ "id": "0f8e3c1e-5d2a-4f6b-9a71-2c3d4e5f6a7b", "type": "availability", "occurred_at": "2026-10-15T09:40:00.123Z", "villas": [ { "id": "01990c1e-7a4b-7c21-9f3e-2b1d5c6a7e80", "slug": "villa-defne", "slug_en": "villa-laurel" }, { "id": "01990c1f-2b3c-7d4e-8f50-6a7b8c9d0e1f", "slug": "villa-ardic", "slug_en": "villa-ardic", "previous_slug": "ardic-villa", "previous_slug_en": "ardic-villa" } ], "scopes": ["calendar"]}| Alan | Açıklama |
|---|---|
id |
Olay kimliği; X-Trimango-Event-Id başlığıyla aynı. Yeniden denemede değişmez. |
type |
Olay türü (bkz. Türler); X-Trimango-Event-Type başlığıyla aynı. |
occurred_at |
Olaydaki son değişikliğin zamanı (ISO 8601, UTC). |
villas |
Etkilenen villalar, her biri bir kez: id, güncel slug ve İngilizce adres slug_en (çeviri yoksa slug ile aynı). reference ve test olaylarında ve all_villas kapsamında boş dizi. |
villas[].previous_slug, previous_slug_en |
Yalnız villanın adresi bu olayla değiştiyse: değişiklikten önceki adres. |
scopes |
Değişen bölümler (aşağıdaki tablo). |
Villaları kendi kayıtlarınızda id ile eşleyin. Slug değişebilir; previous_slug geldiğinde kendi tarafınızdaki slug’ı güncelleyin.
İmza doğrulama
Bölüm başlığı “İmza doğrulama”Her isteği işlemeden önce doğrulayın. İmza şöyle hesaplanır:
- İmzalanan metin:
X-Trimango-Timestampbaşlığındaki değer (Unix saniye, ondalıksız), bir nokta (.) ve istek gövdesinin ham baytları. - İmza bu metnin HMAC-SHA256’sıdır. Anahtar, imza anahtarının kendisidir:
whsec_öneki dahil, UTF-8. X-Trimango-Signaturebaşlığısha256=ve küçük harfli hex özetten oluşur.
Kurallar:
- Gövdeyi ham kullanın. JSON olarak ayrıştırıp yeniden yazarsanız (boşluk, alan sırası, Unicode kaçışları) imza tutmaz. Gövdeyi bayt olarak okuyun, imzayı doğrulayın, sonra ayrıştırın.
- Zaman damgasını denetleyin. Damga sizin saatinize göre 5 dakikadan eski ya da 5 dakikadan ileri ise imza doğru olsa bile isteği reddedin (401). Damga imzaya dahil olduğu için değiştirilemez; yakalanan bir istek 5 dakika sonra işe yaramaz. Sunucu saatinizi NTP ile eşitleyin.
- Sabit sürede karşılaştırın (
timingSafeEqual,hash_equals,FixedTimeEquals). - Önce zaman damgasını, sonra imzayı denetleyin. Biri tutmazsa 401 dönün.
Yeniden denemelerde zaman damgası ve imza yenilenir; gecikmiş bir deneme bu yüzden reddedilmez.
Node örneği
Bölüm başlığı “Node örneği”Bağımlılığı olmayan, çalışır bir sunucu (Node 18 ve üstü):
// Çalıştırma: TRIMANGO_WEBHOOK_SECRET=whsec_… node webhook.mjsimport crypto from "node:crypto";import http from "node:http";
const SECRET = process.env.TRIMANGO_WEBHOOK_SECRET; // whsec_… (önekiyle birlikte)const MAX_SKEW_SECONDS = 300; // 5 dakikaconst MAX_BODY_BYTES = 1024 * 1024;
/** İmza ve zaman damgası doğruysa true. rawBody: gövdenin ham baytları (Buffer). */export function verifyTrimangoSignature(rawBody, timestamp, signature, secret, nowSeconds = Date.now() / 1000) { if (typeof timestamp !== "string" || !/^\d+$/.test(timestamp)) return false; if (Math.abs(nowSeconds - Number(timestamp)) > MAX_SKEW_SECONDS) return false; const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.`, "utf8").update(rawBody).digest("hex"); if (typeof signature !== "string" || signature.length !== expected.length) return false; return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));}
const processed = new Set(); // örnek; gerçekte kalıcı bir depo (veritabanı tablosu, Redis) kullanın
async function handleEvent(event) { if (processed.has(event.id)) return; // aynı olay ikinci kez geldi switch (event.type) { case "availability": // event.villas: takvimi değişen villalar → müsaitliği yeniden çekin case "price": // scopes "prices" ya da "deals" (+ "all_villas") → fiyat önbelleğini atın case "villa": // scopes "detail", "photos", "status" → villa kaydını yeniden çekin case "reference": // scopes "locations", "dictionaries", "currency" → sözlükleri ve TL tutarları yenileyin await enqueueRefresh(event); // kendi kuyruğunuza bırakın; yanıtı bekletmeyin break; default: // "test", "content" ve tanımadığınız türler: bir şey yapmayın break; } processed.add(event.id);}
async function enqueueRefresh(event) { console.log("yenilenecek", event.type, event.scopes, event.villas.map((v) => v.id));}
http .createServer((req, res) => { if (req.method !== "POST" || req.url !== "/trimango/webhook") { res.writeHead(404).end(); return; } const chunks = []; let size = 0; req.on("data", (chunk) => { size += chunk.length; if (size > MAX_BODY_BYTES) req.destroy(); else chunks.push(chunk); }); req.on("end", () => { const rawBody = Buffer.concat(chunks); // JSON'a çevirip yeniden yazmadan, geldiği gibi const ok = verifyTrimangoSignature( rawBody, req.headers["x-trimango-timestamp"], req.headers["x-trimango-signature"], SECRET, ); if (!ok) { res.writeHead(401).end(); return; } const event = JSON.parse(rawBody.toString("utf8")); res.writeHead(204).end(); // önce yanıt: 10 saniye sınırı handleEvent(event).catch((err) => console.error("webhook işlenemedi", event.id, err)); }); }) .listen(process.env.PORT ?? 3000);Express kullanıyorsanız gövdeyi ham almak için o yolda express.json() yerine express.raw() kullanın:
app.post("/trimango/webhook", express.raw({ type: "application/json" }), (req, res) => { const ok = verifyTrimangoSignature(req.body, req.get("x-trimango-timestamp"), req.get("x-trimango-signature"), SECRET); if (!ok) return res.sendStatus(401); res.sendStatus(204); handleEvent(JSON.parse(req.body.toString("utf8"))).catch(console.error);});PHP ve C# örnekleri
Bölüm başlığı “PHP ve C# örnekleri”<?phpfunction trimango_verify(string $raw, string $timestamp, string $signature, string $secret): bool { if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) { return false; } $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret); return hash_equals($expected, $signature);}
$raw = file_get_contents('php://input'); // ham gövde$ok = trimango_verify( $raw, $_SERVER['HTTP_X_TRIMANGO_TIMESTAMP'] ?? '', $_SERVER['HTTP_X_TRIMANGO_SIGNATURE'] ?? '', getenv('TRIMANGO_WEBHOOK_SECRET'));if (!$ok) { http_response_code(401); exit;}$event = json_decode($raw, true);http_response_code(204);using System.Security.Cryptography;using System.Text;
static bool VerifyTrimangoSignature(byte[] rawBody, string? timestamp, string? signature, string secret, long? nowSeconds = null){ if (string.IsNullOrEmpty(timestamp) || !timestamp.All(c => c is >= '0' and <= '9')) return false; var now = nowSeconds ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (Math.Abs(now - long.Parse(timestamp)) > 300) return false; var message = Encoding.UTF8.GetBytes(timestamp + ".").Concat(rawBody).ToArray(); var hash = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), message); var expected = "sha256=" + Convert.ToHexString(hash).ToLowerInvariant(); return signature is not null && CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(signature));}Kendi doğrulamanızı sınamak
Bölüm başlığı “Kendi doğrulamanızı sınamak”Şu girdiyle imza fonksiyonunuz aynı sonucu vermelidir (zaman denetimini bu sınama için 1760518800 anına göre yapın):
| Girdi | Değer |
|---|---|
| İmza anahtarı | whsec_ornekanahtar |
X-Trimango-Timestamp |
1760518800 |
| Gövde (tek satır, boşluksuz) | {"id":"0f8e3c1e-5d2a-4f6b-9a71-2c3d4e5f6a7b","type":"test","occurred_at":"2026-10-15T09:40:00.123Z","villas":[],"scopes":[]} |
Beklenen X-Trimango-Signature |
sha256=43a932b23526fe26f9b52a05edccc032c081c03a3e9971ac351d8094330411a0 |
Türler ve yapılacaklar
Bölüm başlığı “Türler ve yapılacaklar”Olay yalnız “şu değişti” der. Etkilenen veriyi önbelleğinizden atın ya da API’den yeniden çekin.
type |
scopes |
Ne zaman | Ne yapmalısınız |
|---|---|---|---|
availability |
calendar |
Opsiyon ya da rezervasyon açıldı, iptal edildi, süresi doldu, tarihi ya da villası değişti; takvim kapatması, ev sahibi kullanımı, dış takvim eşitlemesi; gece fiyatı | villas[] için müsaitliği (GET /availability/months) yeniden çekin; bu villalar için sakladığınız teklifleri geçersiz sayın. |
price |
prices |
Villanın sezon fiyatları ve ücretleri değişti | villas[] için fiyat önbelleğinizi ve sakladığınız teklifleri atın; gerekiyorsa yeniden teklif alın. |
price |
deals + all_villas |
Kampanya, erken rezervasyon, son dakika ya da hediye gece yayınlandı, değişti, durduruldu; ayrıca her gece İstanbul saatiyle 00:01’de tarihe bağlı başlayan ve biten kampanyalar için | villas boştur: bütün villaların fiyat önbelleğini atın. |
villa |
detail |
Villa içeriği: genel bilgiler, konum, özellikler, odalar, kurallar | villas[] için villa ayrıntısını (GET /villas/{slug}) yeniden çekin. previous_slug geldiyse slug’ı güncelleyin. |
villa |
photos |
Fotoğraflar değişti | villas[] için fotoğrafları yeniden çekin. |
villa |
status |
Villa yayına alındı, satışa kapandı, yayından kaldırıldı, arşivlendi ya da geri alındı | villas[] için villayı yeniden çekin. Ayrıntı 404 dönüyorsa villayı satıştan kaldırın. |
reference |
locations |
İl, bölge, lokasyon ya da mesafe yerleri eklendi, değişti, silindi | Lokasyon listesini (GET /locations) ve kendi lokasyon eşlemenizi yenileyin. |
reference |
dictionaries |
Villa tipi, özellik, filtre grubu, havuz tipi, ev kuralı, oda tipi gibi sözlükler değişti | Özellik ve tip listelerini (GET /features, GET /types/counts) ve kendi eşlemenizi yenileyin. |
reference |
currency |
TCMB kuru en az bir para biriminde değişti (hafta içi 10:00 ve 15:45, İstanbul) | Sakladığınız TL tutarları ve teklifleri geçersiz sayın; güncel kurlar GET /site yanıtındaki rates_try’de. |
test |
— | Trimango ekibi test olayı gönderdi | Hiçbir şey; 2xx dönün. |
content |
çeşitli | Trimango’nun web sitesi içerikleri (sayfa, blog, SSS …) | Sizi ilgilendirmez; 2xx dönüp geçin. |
all_villaskapsamı geldiğindevillasboştur: bütün villalar etkilenmiştir (bir kampanya bütün portföyü kapsıyorsa ya da tek olayda 200’den çok villa değiştiyse).referenceolayları villa listesi taşımaz. Villa ayrıntısında lokasyon ya da özellik adlarını saklıyorsanız o kayıtları da yenileyin.- Yeni türler ve kapsamlar eklenebilir. Tanımadığınız türü ya da kapsamı 2xx dönerek geçin; gerekiyorsa geniş bir yenileme yapın. Tanımadığınız bir değer yüzünden hata dönmeyin, yoksa olay boşuna yeniden denenir.
Katalog ve müsaitliği nasıl çekeceğiniz: Katalog, Müsaitlik, Fiyat ve teklif.
Toplama ve sıra
Bölüm başlığı “Toplama ve sıra”- Değişiklikler istemci başına yaklaşık 5 saniye toplanır. Aynı türdeki değişiklikler tek olayda birleşir, villalar tekilleşir.
Bir turda en çok beş olay gelir (
availability,price,villa,content,reference; her türden bir). - Toplama penceresinde villanın adresi birden çok kez değiştiyse
previous_slugen eski adrestir (A → B → C’deA); adres eski hâline döndüyse alan gelmez. - Sıra garanti değildir. Yeniden denenen eski bir olay yenisinden sonra gelebilir. Olay yalnız “yenile” dediği ve siz veriyi her zaman API’den güncel okuduğunuz için sıra önemli değildir. Olaydaki bilgiyle kendi kaydınızı güncellemeye çalışmayın.
- Olay, değişikliği yapan işlemle birlikte kaydedilir: işlem geri alınırsa olay gitmez.
İdempotentlik
Bölüm başlığı “İdempotentlik”Aynı olay birden çok kez gelebilir: yeniden denemede ve Trimango ekibi elle yeniden gönderdiğinde aynı id ile (yeni zaman
damgası ve imzayla). İşlediğiniz olayların id’sini en az birkaç gün saklayın; aynı id yeniden gelirse işlemeden 2xx dönün.
Olaylar yalnız “yenile” dediği için iki kez işlemek zarar vermez, yalnız boşuna istek yapar.
Yanıt ve yeniden deneme
Bölüm başlığı “Yanıt ve yeniden deneme”- 10 saniye içinde 2xx dönün. En iyisi olayı doğrulayıp kendi kuyruğunuza yazmak, hemen 2xx dönmek ve yenilemeyi arka planda yapmaktır. Olayı yanıttan önce kalıcı bir yere yazın; işlemeden önce 2xx dönüp sonra kaybederseniz olay yeniden gönderilmez.
- Yönlendirme (3xx) izlenmez, başarısız sayılır. Adresinizi değiştirirseniz Trimango ekibine bildirin.
- 2xx dışı yanıt, zaman aşımı ya da bağlantı hatasında aynı olay 1 dakika, 5 dakika, 30 dakika ve 2 saat sonra yeniden denenir. Beşinci denemeden sonra vazgeçilir.
