# StreamHost: Third-Party PMS Reservation Webhook

Push reservations from any PMS into StreamHost. Last verified 2026-10-01.

`{{WEBHOOK_URL}}` is the host's webhook URL and `{{DRY_RUN_URL}}` is the same URL with `validate/` added. The host copies the URL and the token from their StreamHost dashboard (**Connect your own PMS**).

## Overview

Send StreamHost one HTTP request every time a reservation is **created, changed or cancelled** in your PMS. StreamHost keeps the host's reservations in sync and takes care of the rest: WhatsApp messages to the guest, deposit collection, the pre-arrival guest-information request and housekeeping.

The integration is push-only. StreamHost never calls your PMS, and you never need to poll.

**How a booking flows**

1. Your PMS sends the reservation as JSON to the host's webhook URL, with the host's token.
2. StreamHost replies straight away to confirm it was received.
3. StreamHost finds the room from your `unit_id`, creates or updates the reservation, and schedules the guest's messages.

## Quick start

1. **Get the URL and token.** The host opens the StreamHost dashboard → **Connect your own PMS** → **Generate token**, and sends you the **Webhook URL** and the **token** shown there. The token is shown only once.
2. **Map rooms.** In **Buildings**, the host sets each unit's **External ID** to the room id your PMS uses. That's what you send as `unit_id`.
3. **Test it.** Paste the token into the **Try it** panel on this page and run an example. Nothing is saved and no guest is messaged.
4. **Go live.** Send `reservation.created`, `reservation.updated` and `reservation.deleted` events to the webhook URL.
5. **Check deliveries.** The same dashboard window lists every request StreamHost received and whether it was accepted.

**Example request**

```bash
curl -X POST '{{WEBHOOK_URL}}' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: <TOKEN>' \
  -d '{
    "event": "reservation.created",
    "data": {
      "reservation_id": "res_10293",
      "property_id": "prop_tower_a",
      "unit_id": "unit_a_12_03",
      "guest": { "name": "Aisha Rahman", "phone": "+60123456789" },
      "check_in": "2026-11-01",
      "check_out": "2026-11-04",
      "source": "airbnb"
    }
  }'
```

## URL & authentication

Both values come from the host's StreamHost dashboard (**Connect your own PMS**):

- **Webhook URL**: where you send requests. Shown as `{{WEBHOOK_URL}}` throughout these docs.
- **Token**: identifies the host. Send it in the `Authorization` header **as-is**, with no `Bearer` or `Token` in front.

**Header**

```http
Authorization: 3f9c1e...a07b
```

> **Warning:** `Authorization: Bearer <token>` is rejected with `401`. Send the token on its own.

**One token per host.** The token is how StreamHost knows which host a booking belongs to. If you serve several hosts, store each host's token and send each host's bookings with their own.

**Keep it secret.** Treat the token like a password: keep it on your server, send it only over HTTPS, and ask the host to rotate it if it leaks. After a rotation the old token stops working immediately, so switch to the new one first.

## Endpoint

**Request**

```http
POST {{WEBHOOK_URL}}
Content-Type: application/json
Authorization: <TOKEN>
```

One URL for every event. The `event` field says what happened.

## Payload fields

The body has two keys, `event` and `data`. **Always send the full current reservation**, not just the fields that changed.

#### `Envelope`
| Field | Type | Required | Example | What it's for |
| --- | --- | --- | --- | --- |
| `event` | string | Required | `"reservation.created"` | What happened: `reservation.created`, `reservation.updated` or `reservation.deleted`. |
| `data` | object | Required | `{ … }` | The reservation. |

#### `data`
| Field | Type | Required | Example | What it's for |
| --- | --- | --- | --- | --- |
| `reservation_id` | string | Required | `"res_10293"` | Your PMS's id for the booking. Keep it the same for the life of the booking; StreamHost uses it to recognise updates and cancellations, so sending the same booking twice never creates a duplicate. |
| `unit_id` | string | Required | `"unit_a_12_03"` | Your PMS's id for the room. Must match the **External ID** the host set on the unit in StreamHost. It tells StreamHost which room and building the guest is in, which decides the door code, WiFi, check-in instructions and the WhatsApp number used. |
| `property_id` | string | Required | `"prop_tower_a"` | Your PMS's id for the property. Kept for reference and troubleshooting. |
| `check_in` | string (YYYY-MM-DD) | Required | `"2026-11-01"` | Arrival date. Pre-arrival and check-in messages are timed from it. |
| `check_out` | string (YYYY-MM-DD) | Required | `"2026-11-04"` | Departure date, after `check_in`. Check-out messages and housekeeping are timed from it. |
| `guest` | object | Required | `{ name, phone, email }` | Who is staying, so StreamHost can message them. See `data.guest` below. |
| `source` | string | Optional | `"airbnb"` | Where the booking came from, as one of the slugs in **Source (OTA) slugs**. Hosts turn deposits and the guest-information request on or off per channel, so send it whenever you know it. |
| `status` | string | Optional | `"Checked-In"` | The booking's current state, used to mark the guest checked in or out. A status containing "cancel" or "no show" cancels the booking. See **Status values**. |
| `ota_id` | string | Optional | `"HMABCD1234"` | The OTA's own confirmation code (Airbnb, Booking.com…), shown to the host's staff. |
| `unit_name` | string | Optional | `"A-12-03"` | The room's name, shown to staff if `unit_id` hasn't been mapped yet. |
| `property_name` | string | Optional | `"Tower A"` | The property's name, for reference. |
| `no_of_pax` | integer | Optional | `2` | Number of guests, shown to staff and housekeeping. Defaults to 1. |

#### `data.guest`
| Field | Type | Required | Example | What it's for |
| --- | --- | --- | --- | --- |
| `name` | string | Required | `"Aisha Rahman"` | Shown in the host's inbox and used to greet the guest in messages. |
| `phone` | string | Required | `"+60123456789"` | The guest's WhatsApp number, **with country code**. If the guest has no phone, send `email` instead. |
| `email` | string | Optional | `"aisha@example.com"` | Used for email messages, and to reach guests who have no phone. |
| `id` | string | Optional | `"US.13491208655302741918"` | The guest's WhatsApp or WeChat user id, if your PMS has it. Helps StreamHost recognise a returning guest who messages from a different number. Not your internal guest id. |

## Events

### reservation.created
A new booking. StreamHost adds it and schedules the guest's messages. Sending it again for the same `reservation_id` is safe.

### reservation.updated
Any change: dates, room, guest details, status. Send the full reservation; StreamHost works out what changed and only re-sends messages the change affects. Sending an update with no changes is safe.

### reservation.deleted
The booking was cancelled. StreamHost **permanently removes** the reservation and its upcoming messages. The guest's chat history stays. If the same `reservation_id` is sent again later, it's treated as a new booking.

> **Warning:** A `status` containing "cancel" or "no show" also cancels the booking, whatever the `event`. Don't send a status like `"Payment cancelled"` for a booking that's still going ahead.

## Status values

`status` is optional and not case-sensitive. If your PMS doesn't track these states, leave it out.

| You send | Result |
| --- | --- |
| anything containing `cancel`, `no_show`, `no show`, `noshow` | Booking **cancelled** |
| `Arrived`, `Arrival`, `Check-In`, `Checked-In`, `Checked In`, `In House`, `InHouse` | Guest marked **checked in** |
| `Departed`, `Departure`, `Check-Out`, `Checked-Out`, `Checked Out` | Guest marked **checked out**; housekeeping is notified |
| `Confirmed`, `Reserved`, `Pending`, or anything else | No change |

## Source (OTA) slugs

Send one of these exact values in `source` (not case-sensitive). `bookingcom` works; `Booking.com` doesn't.

| Slug | Channel |
| --- | --- |
| `airbnb` | Airbnb |
| `bookingcom` | Booking.com |
| `agoda` | Agoda |
| `agodahomes` | Agoda Homes |
| `expedia` | Expedia |
| `vrbo` | VRBO |
| `traveloka` | Traveloka |
| `tiketcom` | Tiket.com |
| `ctripcm` | Ctrip / Trip.com |
| `streamhost` | Direct / walk-in booking |
| `hostplatform` | HostPlatform |
| `facebook` | Facebook |
| `instagram` | Instagram |
| `tiktok` | TikTok |
| `xhs` | Xiaohongshu (RED) |
| `youtube` | YouTube |
| `lemon8` | Lemon8 |

Any other value is grouped under "Other OTA". Send direct bookings as `streamhost`.

## Matching rooms

`unit_id` must equal the **External ID** of a unit in the host's StreamHost account. The building comes from that unit.

If `unit_id` doesn't match any unit, the booking is still saved, in a holding area called **Unsynced Units**, so the host can see it and move the room to the right building. Later bookings for that room then go straight there.

> **Warning:** Always send the real room id. Never send placeholder values such as `"[]"`, `"null"` or `"0"`; the guest could be given another room's details.

## WhatsApp number per building

Hosts with several WhatsApp numbers can give each building its own number in StreamHost. Guests are messaged from the number of the building their room is in, or from the host's default number if none is set.

**Nothing to do on your side.** There's no field for this. Send the correct `unit_id` and StreamHost picks the number. The **Try it** panel shows which number a booking will use.

## Responses & errors

| Status | Body | Meaning |
| --- | --- | --- |
| `200` | `{"status": "ok"}` | Received. |
| `400` | `{"error": "invalid payload"}` | The body is missing a required field or isn't in the expected shape. Fix the payload; retrying the same body won't help. |
| `401` | `{"error": "unauthorized"}` | The token is missing or wrong. Check it isn't prefixed with `Bearer `. |

Use the **Try it** panel or the dry run to see exactly what's wrong with a payload. The host can also see every request StreamHost received in their dashboard.

## Retries & ordering

- **Retry on network errors, timeouts and `5xx`.** Sending the same event twice is always safe.
- **Don't retry `400` or `401`.** The same request will fail the same way.
- **Send events in order** and always include the full current reservation. If two updates arrive out of order, the last one received wins.
- **After an outage,** resend every active reservation as `reservation.created`. Nothing is duplicated.

## Testing with dry run

`POST {{DRY_RUN_URL}}` (your webhook URL with `validate/` added) takes the **same token and body** as the webhook, but **saves nothing and messages no one**. It returns:

- `would_respond`: the response the webhook would give.
- `errors`: everything that must be fixed, with the field it applies to.
- `warnings`: things that will be accepted but probably aren't what you meant, such as an unmapped `unit_id` or an unknown `source`.
- `outcome`: what would happen: created, updated or cancelled, which room and building, and which WhatsApp number messages the guest.

The **Try it** panel on this page uses it. You can call it from your own tests too (up to 300 requests an hour).

**Dry-run response**

```json
{
  "would_respond": { "status": 200, "body": { "status": "ok" } },
  "errors": [],
  "warnings": [
    { "field": "data.source", "message": "'Booking.com' is not a recognised slug. …" }
  ],
  "outcome": {
    "action": "create",
    "reservation_exists": false,
    "unit": { "matched": true, "unit_name": "A-12-03", "building_name": "Tower A" },
    "whatsapp_number": { "source": "building", "display_phone_number": "+60 11-1111 2222", "verified_name": "Tower A Concierge" },
    "channel": { "value": "booking.com", "recognised": false },
    "lifecycle": null,
    "messages_scheduled": true
  }
}
```

## Go-live checklist

- Webhook URL and token taken from the host's dashboard, stored per host, never logged.
- Token sent as-is in `Authorization`, with no `Bearer`.
- Every room's **External ID** in StreamHost equals the `unit_id` you send.
- `reservation_id` stays the same for the life of the booking.
- Dates are `YYYY-MM-DD`, and `check_out` is after `check_in`.
- Guest phone includes the country code.
- `source` uses the slugs listed above.
- Cancellations are sent as `reservation.deleted`.
- Network errors and `5xx` are retried; `400` and `401` are not.
- The dry run shows no errors or warnings for a new, a changed and a cancelled booking.
