İçeriğe geç

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.

  1. Sunucunuzda https:// ile başlayan, POST kabul eden bir adres açın (ör. https://api.ornek.com/trimango/webhook).
  2. Adresi Trimango ekibine iletin. Ekip adresi API istemcinize tanımlar.
  3. 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).
  4. 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.1
Content-Type: application/json; charset=utf-8
User-Agent: Trimango-Webhook/1
X-Trimango-Event-Id: 0f8e3c1e-5d2a-4f6b-9a71-2c3d4e5f6a7b
X-Trimango-Event-Type: availability
X-Trimango-Timestamp: 1760518800
X-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.

Her isteği işlemeden önce doğrulayın. İmza şöyle hesaplanır:

  1. İmzalanan metin: X-Trimango-Timestamp başlığındaki değer (Unix saniye, ondalıksız), bir nokta (.) ve istek gövdesinin ham baytları.
  2. İmza bu metnin HMAC-SHA256’sıdır. Anahtar, imza anahtarının kendisidir: whsec_ öneki dahil, UTF-8.
  3. X-Trimango-Signature baş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.

Bağımlılığı olmayan, çalışır bir sunucu (Node 18 ve üstü):

webhook.mjs
// Çalıştırma: TRIMANGO_WEBHOOK_SECRET=whsec_… node webhook.mjs
import 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 dakika
const 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:

express.mjs
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);
});
webhook.php
<?php
function 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);
TrimangoWebhook.cs
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));
}

Ş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

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_villas kapsamı geldiğinde villas boş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).
  • reference olayları 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.

  • 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_slug en eski adrestir (A → B → C’de A); 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.

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.

  • 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.