StayBindDevelopers
Connect a custom OTA

Map reservations

How StayBind normalizes your reservation payload into its canonical model, the default shape, writing a custom mapper, and how a connection's credentials are stored.

StayBind reduces every reservation, from any source, to one CanonicalReservation. For a custom OTA, a small mapReservation function does that translation. You have two options: send the default shape and it works as-is, or send your own shape and StayBind configures a mapper for it.

The default shape

Out of the box, the mapper reads these fields from each raw reservation:

Your fieldMaps toNotes
id or reservationIdproviderResIdper-delivery idempotency key, ingested once
roomTypeId or roomTypeproviderRoomTypeIdresolved to a StayBind unit via the listing mapping
statusstatusnew | modified | cancelled (anything else → new)
checkIn, checkOutdateRangeYYYY-MM-DD, check-out exclusive
channelexternalChanneloptional label for the downstream source
totalMinortotaloptional, integer paise, currency INR
guest.nameguest.namerequired (defaults to "Guest" if absent)
guest.email, guest.phoneguest.email, guest.phoneoptional
guest.adults, guest.childrenguest.adults, guest.childrendefault 1 / 0

Carry a stable booking id for modify and cancel

The default mapper only sets providerResId. If you send modified or cancelled revisions, your payload must also carry a stable booking id that is constant across revisions, and the mapper must set it as providerBookingId. Without it, a modification looks like a brand-new booking. Mention this when you set up your connection so the mapper includes it.

Writing a custom mapper

If your payload differs, the mapper is a single pure function: raw payload in, canonical reservation out. This is the only per-partner code, everything else (HTTP, retries, signature checks, ingestion) is shared.

mapReservation signature
type MapReservation = (raw: unknown) => CanonicalReservation;
Example: mapping a custom payload
const mapReservation: MapReservation = (raw) => {
  const r = raw as MyOtaReservation;
  return {
    provider: "partner",
    externalChannel: "my-ota",
    providerResId: r.event_id,        // unique per delivery
    providerBookingId: r.booking_ref, // stable across revisions
    providerRoomTypeId: r.room_code,
    status: r.cancelled ? "cancelled" : r.amended ? "modified" : "new",
    dateRange: { checkIn: r.arrival, checkOut: r.departure },
    guest: {
      name: r.customer.full_name,
      email: r.customer.email,
      phone: r.customer.mobile,
      adults: r.pax.adults ?? 1,
      children: r.pax.children ?? 0,
    },
    total: { amount: r.total_paise, currency: "INR" },
    raw, // keep the original for the audit trail
  };
};

The full set of canonical fields (including the optional ackRef, ratePlanId, and bookedAt) is documented in the canonical model.

Registering the adapter

On the StayBind server, your OTA is registered as a PartnerChannelAdapter with your mapper (and, recommended, a signature verifier):

server registration (StayBind side)
new PartnerChannelAdapter({
  mapReservation,
  // verify the inbound webhook against the connection's webhookSecret
  verifySignature: (event, secret) => event.signature === secret,
});

This is platform-side wiring; as the OTA you provide the payload shape (or confirm you'll send the default), and the StayBind team or a self-hosting operator registers it.

Credentials

A connection stores only a pointer (secretRef); the actual secrets live in StayBind's secret store, never in the database. Your connection needs three values:

Prop

Type

In the default deployment these are read from an environment variable named CHANNEL_SECRET_<REF> holding a JSON object:

CHANNEL_SECRET_MYOTA
{ "baseUrl": "https://api.my-ota.com", "apiKey": "…", "webhookSecret": "…" }

The secret store is an interface, so a production deployment can swap the env backend for a vault (KMS, Doppler, Vault) without changing any call site. The sync engine only ever sees the resolved credentials.

Putting it together

The operator creates a partner connection in the Channels screen; StayBind issues the connectionId.
You provide baseUrl, apiKey, webhookSecret, and your reservation payload shape; they're stored against the connection.
You implement the six endpoints and start posting reservations to the webhook.
StayBind begins pushing availability out to you whenever the ledger changes.

Next: Sync availability out.

On this page