RoomFollow Connectivity API
Webhook olayları
Rezervasyon, giriş/çıkış, tahsilat, fatura ve müsaitlik değişikliklerini imzalı HTTPS bildirimleriyle anında alın.
Adres tanımlama
Tesis müdürü RoomFollow panelinde Entegrasyonlar → API erişimi → Webhook adresleri ekranından HTTPS adresini ve olayları seçer. İmza anahtarı whsec_… yalnız oluşturma ve yenileme anında bir kez gösterilir. Bir tesiste en fazla 10 adres tanımlanabilir. Bağlantıyı doğrula düğmesi adrese hemen imzalı bir webhook.test olayı gönderir.
Adres kuralları
- Yalnız https. Adreste kullanıcı adı/parola veya # parçası olamaz; 443 dışında yalnız 1024 ve üzeri portlar.
- Ad DNS ile çözülür; özel ağ, loopback, bağlantı-yerel (169.254.0.0/16 ve bulut metadata dahil), CGNAT, çok noktaya yayın veya ayrılmış bir adrese çözülürse reddedilir. Bağlantı doğrulanan IP adresine açılır.
- Yönlendirmeler (3xx) izlenmez ve başarısız sayılır.
- Zaman aşımı 10 saniyedir; yanıt gövdesi okunmaz (en fazla 64 KiB tüketilir). İstek gövdesi en fazla 256 KiB’tır.
Olaylar
| Olay | Ne zaman |
|---|---|
| reservation.created | Panel, mobil uygulama, kanal veya API ile yeni rezervasyon kaydedildiğinde. |
| reservation.updated | Tarih, kişi sayısı, durum veya diğer rezervasyon alanları değiştiğinde. |
| reservation.cancelled | Rezervasyon iptal durumuna geçtiğinde. |
| guest.checked_in | Bir oda satırı için check-in tamamlandığında. |
| guest.checked_out | Bir oda satırı için check-out tamamlandığında. |
| folio.payment_posted | Folyoya tahsilat veya tahsilat iptali (ters kayıt) işlendiğinde. |
| invoice.issued | Taslak fatura kesinleştirildiğinde veya e-belge olarak gönderildiğinde. |
| availability.changed | Rezervasyon veya oda blokajı müsaitliği etkileyebileceğinde; güncel değeri /availability ucundan okuyun. |
Olaylar işlemi yapan ekrandan bağımsızdır: panel, mobil uygulama, kanal aktarımı veya API ile yapılan değişikliklerin hepsi bildirilir. Olaylar kaydedildikten sonra genellikle 30–60 saniye içinde gönderilir. Yükler kişisel veri içermez (misafir adı, telefon, e-posta, kimlik yoktur); ayrıntı için ilgili okuma uçlarını kullanın. Rezervasyon verisi GET /reservations yanıtıyla aynı alanları taşır. availability.changed bir ipucudur: güncel değeri /availability ucundan okuyun.
{
"id": "evt_3kTq0ExampleOnly9vB2",
"type": "reservation.created",
"apiVersion": "v1",
"createdAt": "2026-10-07T09:00:04.000Z",
"occurredAt": "2026-10-07T09:00:00.000Z",
"property": {
"code": "EXAMPLE"
},
"data": {
"id": "res_example_01",
"code": "2026-000123",
"status": "GUARANTEED",
"source": "DIRECT",
"externalReference": "EXAMPLE-001",
"channel": null,
"arrivalDate": "2026-10-20",
"departureDate": "2026-10-23",
"adults": 2,
"children": 0,
"roomTypes": [
"STD"
],
"cancelledAt": null,
"createdAt": "2026-10-07T09:00:00.000Z",
"updatedAt": "2026-10-07T09:00:00.000Z"
}
}Olay verisi örnekleri
{
"id": "res_example_01",
"code": "2026-000123",
"status": "GUARANTEED",
"source": "DIRECT",
"externalReference": "EXAMPLE-001",
"channel": null,
"arrivalDate": "2026-10-20",
"departureDate": "2026-10-23",
"adults": 3,
"children": 0,
"roomTypes": [
"STD"
],
"cancelledAt": null,
"createdAt": "2026-10-07T09:00:00.000Z",
"updatedAt": "2026-10-07T10:15:00.000Z"
}{
"id": "res_example_01",
"code": "2026-000123",
"status": "CANCELLED",
"source": "DIRECT",
"externalReference": "EXAMPLE-001",
"channel": null,
"arrivalDate": "2026-10-20",
"departureDate": "2026-10-23",
"adults": 2,
"children": 0,
"roomTypes": [
"STD"
],
"cancelledAt": "2026-10-08T08:30:00.000Z",
"createdAt": "2026-10-07T09:00:00.000Z",
"updatedAt": "2026-10-08T08:30:00.000Z"
}{
"reservationId": "res_example_01",
"reservationCode": "2026-000123",
"externalReference": "EXAMPLE-001",
"reservationRoomId": "rr_example_01",
"roomNumber": "101",
"roomTypeCode": "STD",
"arrivalDate": "2026-10-20",
"departureDate": "2026-10-23",
"adults": 2,
"children": 0,
"checkedInAt": "2026-10-20T13:05:00.000Z",
"checkedOutAt": null
}{
"reservationId": "res_example_01",
"reservationCode": "2026-000123",
"externalReference": "EXAMPLE-001",
"reservationRoomId": "rr_example_01",
"roomNumber": "101",
"roomTypeCode": "STD",
"arrivalDate": "2026-10-20",
"departureDate": "2026-10-23",
"adults": 2,
"children": 0,
"checkedInAt": "2026-10-20T13:05:00.000Z",
"checkedOutAt": "2026-10-23T10:40:00.000Z"
}{
"paymentId": "pay_example_01",
"folioId": "fol_example_01",
"folioNumber": "F-000045",
"reservationId": "res_example_01",
"reservationCode": "2026-000123",
"amountMinor": 450000,
"amountScale": 100,
"currency": "TRY",
"methodCode": "CARD",
"methodKind": "CREDIT_CARD",
"isReversal": false,
"reversedPaymentId": null,
"postedAt": "2026-10-23T10:35:00.000Z"
}{
"invoiceId": "inv_example_01",
"series": "RFA",
"number": "2026000000045",
"type": "E_ARSIV",
"status": "ISSUED",
"issueDate": "2026-10-23",
"netMinor": 375000,
"taxMinor": 37500,
"accommodationTaxMinor": 7500,
"totalMinor": 420000,
"amountScale": 100,
"currency": "TRY",
"folioId": "fol_example_01",
"folioNumber": "F-000045",
"reservationId": "res_example_01"
}{
"from": "2026-10-20",
"to": "2026-10-23",
"toExclusive": true,
"roomTypes": [
"STD"
],
"reason": "reservation"
}Başlıklar ve imza
POST /roomfollow/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: RoomFollow-Webhooks/1.0
RoomFollow-Signature: t=1791363604,v1=<HEX_HMAC_SHA256>
RoomFollow-Event-Id: evt_3kTq0ExampleOnly9vB2
RoomFollow-Event-Type: reservation.created
RoomFollow-Delivery-Id: <DELIVERY_ID>
RoomFollow-Delivery-Attempt: 1İmza, ${t}.${ham_gövde} metninin imza anahtarıyla HMAC-SHA256 değeridir (onaltılık). Doğrulamada ham gövdeyi kullanın, zaman damgasını 5 dakikadan eski isteklerde reddedin ve sabit zamanlı karşılaştırma yapın. Anahtar yenilendikten sonraki 24 saat boyunca başlıkta eski anahtarla üretilmiş ikinci bir v1 değeri de bulunur; herhangi birinin eşleşmesi yeterlidir.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.ROOMFOLLOW_WEBHOOK_SECRET; // whsec_...
// İmza ham gövde üzerinden hesaplanır: JSON'u ayrıştırmadan önce okuyun.
app.post("/roomfollow/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("RoomFollow-Signature") ?? "";
const parts = header.split(",").map((part) => part.split("="));
const t = Number(parts.find(([key]) => key === "t")?.[1]);
const signatures = parts.filter(([key]) => key === "v1").map(([, value]) => value);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${req.body.toString("utf8")}`)
.digest("hex");
const valid = signatures.some(
(signature) =>
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)),
);
if (!valid) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
// event.id ile tekilleştirin; aynı olay birden fazla teslim edilebilir.
res.sendStatus(204);
});<?php
$secret = getenv('ROOMFOLLOW_WEBHOOK_SECRET'); // whsec_...
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_ROOMFOLLOW_SIGNATURE'] ?? '';
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, '');
if ($key === 't') $timestamp = (int) $value;
if ($key === 'v1') $signatures[] = $value;
}
if (!$timestamp || abs(time() - $timestamp) > 300) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
$valid = false;
foreach ($signatures as $signature) {
if (hash_equals($expected, $signature)) { $valid = true; }
}
if (!$valid) { http_response_code(400); exit; }
$event = json_decode($payload, true);
// $event['id'] ile tekilleştirin.
http_response_code(204);import hashlib, hmac, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["ROOMFOLLOW_WEBHOOK_SECRET"].encode() # whsec_...
@app.post("/roomfollow/webhooks")
def roomfollow_webhook():
payload = request.get_data() # ham gövde
header = request.headers.get("RoomFollow-Signature", "")
pairs = [part.split("=", 1) for part in header.split(",") if "=" in part]
timestamp = next((int(v) for k, v in pairs if k == "t" and v.isdigit()), None)
signatures = [v for k, v in pairs if k == "v1"]
if timestamp is None or abs(time.time() - timestamp) > 300:
abort(400)
expected = hmac.new(SECRET, f"{timestamp}.".encode() + payload, hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(expected, s) for s in signatures):
abort(400)
event = json.loads(payload)
# event["id"] ile tekilleştirin.
return "", 204Teslimat, yeniden deneme ve kapatma
- 2xx yanıt başarılıdır. Ağır işleri kuyruğa alıp hızlıca 2xx döndürün.
- Diğer yanıtlar, zaman aşımı ve bağlantı hataları üstel aralıklarla yeniden denenir: yaklaşık 30 sn, 2 dk, 8 dk, 32 dk, 2 sa, 8,5 sa ve 12 sa (±%10). İlk denemeden itibaren 24 saat dolunca veya 8 denemeden sonra teslimat başarısız olur.
- Teslimat en az bir kez yapılır; aynı olay birden fazla gelebilir ve sıralama garanti edilmez.
idalanıyla tekilleştirin, tarih alanlarına göre sıralayın. - Adres 24 saat boyunca hiç başarılı yanıt vermezse (en az 5 başarısız deneme) otomatik olarak kapatılır, bekleyen teslimatlar durdurulur ve adresi ekleyen kullanıcıya e-posta ile panel bildirimi gönderilir. Panelden yeniden etkinleştirilebilir.
- Panelin Teslimat geçmişi sekmesi son 30 günün teslimatlarını HTTP kodu, süre ve hatayla gösterir; Tekrar gönder aynı olay kimliğiyle hemen yeniden dener. İstek gövdesi ve anahtar kaydedilmez.
- Durdurulan veya kapatılan adres için olay biriktirilmez. Partner API platform genelinde kapalıyken webhook gönderimi de yapılmaz.