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

OlayNe zaman
reservation.createdPanel, mobil uygulama, kanal veya API ile yeni rezervasyon kaydedildiğinde.
reservation.updatedTarih, kişi sayısı, durum veya diğer rezervasyon alanları değiştiğinde.
reservation.cancelledRezervasyon iptal durumuna geçtiğinde.
guest.checked_inBir oda satırı için check-in tamamlandığında.
guest.checked_outBir oda satırı için check-out tamamlandığında.
folio.payment_postedFolyoya tahsilat veya tahsilat iptali (ters kayıt) işlendiğinde.
invoice.issuedTaslak fatura kesinleştirildiğinde veya e-belge olarak gönderildiğinde.
availability.changedRezervasyon 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.

İstek gövdesi (reservation.created)UTF-8
{
  "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

reservation.updatedUTF-8
{
  "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"
}
reservation.cancelledUTF-8
{
  "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"
}
guest.checked_inUTF-8
{
  "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
}
guest.checked_outUTF-8
{
  "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"
}
folio.payment_postedUTF-8
{
  "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"
}
invoice.issuedUTF-8
{
  "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"
}
availability.changedUTF-8
{
  "from": "2026-10-20",
  "to": "2026-10-23",
  "toExclusive": true,
  "roomTypes": [
    "STD"
  ],
  "reason": "reservation"
}

Başlıklar ve imza

İstek başlıklarıUTF-8
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.

Node.js (Express)UTF-8
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);
});
PHPUTF-8
<?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);
Python (Flask)UTF-8
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 "", 204

Teslimat, 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. id alanı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.