> ## Documentation Index
> Fetch the complete documentation index at: https://boxo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Hotels Integration

> Host app integration guide for the hotels miniapp — events, payments, and booking state.

For host app owners integrating the hotels miniapp.

This document defines every event sent by the hotels miniapp to a host app, and every host-app request concerning a hotel order. It covers payloads, triggers, authentication, retries, and integration flow.

All instructions use the host-app perspective: endpoints exposed by the host app, Boxo Platform endpoints called by the host app, and the data exchanged through each.

It is written to sit alongside the [flights guide](/mini-apps/flights/integration): the envelope, the five event types, the status model, and the delivery rules are identical, so a host app that already consumes flight order events consumes hotel order events with the same handler. Where the two differ — the shape of `booking`, and the payment integration — the difference is called out.

## How the integration fits together

The host app and hotels miniapp do not call each other directly for events. The Boxo Platform mediates.

```mermaid theme={"system"}
flowchart LR
  HostApp["Host app"] <--> Platform["Boxo Platform"] <--> Miniapp["Hotels miniapp"]
```

Event deliveries arrive from the Boxo Platform and use the host app's credentials.

### Endpoints exposed by the host app

The host app must expose the following endpoints and register each URL in the Boxo Dashboard. The platform authenticates with Basic `base64(hostapp_client_id:hostapp_secret_key)`, using the host-app credentials configured in the dashboard.

| Endpoint                 | Method            | Purpose                                                                 | Required for                           |
| ------------------------ | ----------------- | ----------------------------------------------------------------------- | -------------------------------------- |
| Event receiver           | `POST`            | Receives every hotel order event                                        | All order events                       |
| Create order payment     | `POST`            | Creates a host-app payment and returns `order_payment_id`               | Checkout                               |
| Get order payment status | `POST` (or `GET`) | Returns the current status of a payment                                 | Recovery when a payment result is lost |
| Boxo Connect endpoints   | —                 | Identify the guest. OAuth (get access token + get user data) or Direct. | Login                                  |

<Warning>
  The **Get order payment status** endpoint is not optional in practice. Payment results do get lost in transit, and this is the only way an order stuck mid-payment ever resolves — otherwise a guest can be charged and never given a room.
</Warning>

These are the same two endpoints the flights miniapp uses, with the same bodies, so a host app that has already built them serves both products.

### Boxo endpoints called by the host app

| Operation                         | Method | Endpoint                                         |
| --------------------------------- | ------ | ------------------------------------------------ |
| Report a payment result           | `POST` | `/api/v1/hotels/payments/payment-notify/{slug}/` |
| Read an order's current state     | `GET`  | `/api/v1/hotels/payments/{payment_id}/`          |
| Read a booking                    | `GET`  | `/api/v1/hotels/bookings/{booking_id}/`          |
| Connect a user (Direct flow only) | `POST` | `/api/v1/connect/`                               |

<Note>
  **One difference from flights.** The host app reports the payment result to the hotels backend directly, rather than through the platform's `complete-order`. The body is smaller and the endpoint always answers `200` — see [Report the payment result](#report-the-payment-result).
</Note>

### Dashboard settings to confirm

| Setting                                                    | Why                                                          |
| ---------------------------------------------------------- | ------------------------------------------------------------ |
| **Event receiver enabled**, with the host-app receiver URL | Off by default. No order events are sent until enabled.      |
| `hostapp_client_id` / `hostapp_secret_key`                 | Used to authenticate event deliveries to the host app        |
| `app_id` / `secret_key`                                    | Used by the host app to authenticate its payment-result call |
| Enabled event types                                        | Optional. Empty means all five.                              |

<Note>
  Boxo must enable Event Bridge for both the host app and the hotels miniapp. Enabling only one side prevents event delivery.
</Note>

## Authentication

**Calls received by the host app** (event receiver) carry:

```
Authorization: Basic base64({hostapp_client_id}:{hostapp_secret_key})
Content-Type: application/json
```

The host app must verify these credentials on every request. There is no signature over the body. Host-app endpoints should answer **`200` in all cases**, using a response field to report problems; a non-200 response is treated as a delivery failure and retried.

The scheme word is configurable per host app. It is `Basic` unless the host app's receiver expects `Token`.

**Calls made by the host app** to report a payment result carry:

```
Authorization: Basic base64({app_id}:{secret_key})
X-Miniapp-App-Id: {app_id}
X-Hostapp-Client-Id: {client_id}
Content-Type: application/json
```

<Note>
  IP whitelisting and request signaturing are also available for securing server-to-server calls. See [Security measures](/host-apps/SecurityMeasures).
</Note>

## What an event delivery looks like

Every hotel order event arrives at the host app's **event receiver** in the platform's standard envelope:

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "client_id": "host_XXXXXXXXXXXX",
  "event_type": "order.succeeded",
  "payload": { }
}
```

| Field        | Notes                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------- |
| `app_id`     | The hotels miniapp identifier. Host apps serving several miniapps should route on this field. |
| `client_id`  | Host app identifier                                                                           |
| `event_type` | One of the five types in [The five event types](#the-five-event-types)                        |
| `payload`    | The order body — [Payload reference](#payload-reference). Identical shape for every type.     |

Return HTTP `200`. The response body is forwarded to the hotels miniapp, but the hotel event flow does not depend on its contents; `{}` is sufficient.

<Tip>
  The receiver may also receive event types from other miniapps and the platform. Unsupported event types should be acknowledged with `200` and ignored.
</Tip>

## The five event types

The event type identifies the outcome and can be used for routing without inspecting the payload. They are the same five the flights miniapp emits.

All five lifecycle events require a settled host-app payment. At the time an event is first emitted, `payment_status` is `PAID`. A pending, cancelled, or failed payment never produces a lifecycle event.

| Type                | Fires when                                                               |
| ------------------- | ------------------------------------------------------------------------ |
| `order.succeeded`   | The room exists. The hotel confirmed the reservation.                    |
| `order.failed`      | The guest was charged and holds no reservation.                          |
| `order.refunded`    | Money is owed back to the guest. Carries the exact net amount to return. |
| `booking.changed`   | An existing stay was rewritten — new dates or occupancy.                 |
| `booking.cancelled` | The stay is cancelled, by the guest, the hotel, or support.              |

Nothing is sent while an order is still moving — awaiting payment, or paid with the supplier yet to answer. Events communicate final outcomes rather than intermediate steps.

### `order.succeeded`

Sent once the supplier confirms the room. The host app should use this event to add or confirm the booking. `booking` contains everything required to render a booking card: hotel name, address, images, room, bed arrangement, guests, and the cancellation policy already rendered as display strings.

<Warning>
  **This event can arrive twice for one order, and the second one matters.** `booking_reference` is the hotel's own confirmation number — what the guest quotes at the front desk — and the hotel issues it *after* the reservation confirms: within a minute at one supplier, hours later at another. Until it exists, `booking_reference` is `""`. When it lands, `order.succeeded` is sent again with the field populated and nothing else changed.
</Warning>

A receiver that keys on `miniapp_order_id` and overwrites its record from the payload needs no special handling for this. Do not treat the repeat as a second booking.

### `order.failed`

The guest was charged and no room was secured — the rate sold out between the quote and the booking, the price moved beyond tolerance, or the supplier refused.

<Warning>
  **Not sent when the host app declines the charge.** A payment rejected by the host app is already known to it, and an unpaid order must not appear as a failed booking.
</Warning>

`payment_error_code` carries the machine reason: `sold_out`, `rate_changed_0.0500`, `check_price_failed`, `book_failed_…`. `booking` describes the stay that was attempted, with an empty `reference` and no voucher. `booking_id` is `""` because no booking record was ever written.

`order.refunded` follows.

### `order.refunded`

Sent when a refund obligation is opened, not when it later completes. The host app must return `refund.amount` in `refund.currency` against `refund.source_order_payment_id`, which is the order payment id the host app settled.

`refund.amount` is **already net** of any hotel cancellation penalty. `penalty_amount` is informational and must not be subtracted again.

| `refund.reason`      | Meaning                                                     |
| -------------------- | ----------------------------------------------------------- |
| `fulfillment_failed` | The guest was charged and no room was produced              |
| `booking_cancelled`  | A cancellation was accepted; `amount` is the net refund     |
| `booking_changed`    | A change produced a credit; the original booking stays paid |

A refund is **never** a new order payment, and no new order id appears. It is an operation against the payment the host app already settled.

### `booking.changed`

The stay moved. The payload carries the **new** dates, price and cancellation policy. `status` is still `BOOKED` and `miniapp_order_id` is unchanged — this updates an existing booking, it is not a second stay.

A change that costs more is funded by a second order payment, named in `change.delta_payment_id`. The original `miniapp_order_id` stays on the order. A change that costs less is refunded and reported through `order.refunded`; it is never sent as a negative delta.

Fires on a confirmed change, whether the guest made it in the miniapp or support made it on their behalf.

### `booking.cancelled`

The stay has ended. If a refund is due, a separate `order.refunded` follows.

Fires for a guest cancellation, a cancellation made by Boxo operations, **and a cancellation the hotel or supplier made on their own**. The last case is found by a sweep that re-reads unresolved reservations every five minutes, so it reaches the host app without anyone at Boxo acting.

## Payload reference

The `payload` object of every order event. **All keys are always present.** Missing data is `""` or `null`, never an omitted key. Amounts are decimal **strings**, never numbers.

```json theme={"system"}
{
  "miniapp_order_id": "08fa8e4d-6633-469e-b6be-8f3f926f8f34",
  "booking_id": "af523b33-4c3a-496c-9563-27ee92f01081",
  "status": "BOOKED",
  "payment_status": "PAID",
  "fulfillment_status": "COMPLETED",
  "booking_reference": "880928193161802",
  "total_amount": "922.86",
  "total_currency": "USD",
  "destination": "London, GB",
  "check_in": "2026-10-21",
  "check_out": "2026-10-24",
  "nights": 3,
  "payment_error_code": "",
  "booking": { },
  "price": { },
  "payment": { },
  "refund": null,
  "change": null
}
```

### Top level

| Field                   | Type             | Notes                                                                                                                                                     |
| ----------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `miniapp_order_id`      | string (UUID)    | The order id. Stable across every event for that order, and identical to the order payment id created at checkout. **Use it as the host-app record key.** |
| `booking_id`            | string (UUID)    | Boxo's booking id. `""` when the order failed before a booking record existed.                                                                            |
| `status`                | string           | One-word summary. See [Reading status](#reading-status).                                                                                                  |
| `payment_status`        | string           | `PENDING` \| `PAID` \| `FAILED` \| `REFUNDED`                                                                                                             |
| `fulfillment_status`    | string           | `PENDING` \| `PROCESSING` \| `COMPLETED` \| `FAILED` \| `CANCELLED`                                                                                       |
| `booking_reference`     | string           | The **hotel's** confirmation number. `""` until the hotel issues it. Same as `booking.reference`.                                                         |
| `total_amount`          | decimal string   | What the guest was charged                                                                                                                                |
| `total_currency`        | string           | ISO 4217 code for `total_amount`                                                                                                                          |
| `destination`           | string           | `{city}, {country_code}`, e.g. `London, GB`. `""` before a property is chosen.                                                                            |
| `check_in`, `check_out` | string           | Calendar dates at the property, `YYYY-MM-DD`                                                                                                              |
| `nights`                | int              | Length of stay. `0` before dates exist.                                                                                                                   |
| `payment_error_code`    | string           | Set when something went wrong with payment or booking; otherwise `""`                                                                                     |
| `booking`               | object \| `null` | The stay. See [`booking`](#booking). `null` only for an order that failed before a stay was assembled.                                                    |
| `price`                 | object           | Always present. See [`price`](#price).                                                                                                                    |
| `payment`               | object           | Always present. See [`payment`](#payment).                                                                                                                |
| `refund`                | object \| `null` | Present from the moment a refund is opened; `null` otherwise. See [`refund`](#refund--from-the-moment-one-is-opened).                                     |
| `change`                | object \| `null` | Present on `booking.changed`; `null` otherwise. See [`change`](#change--on-bookingchanged-only).                                                          |

<Note>
  **A hotel night is a local date at the property.** `check_in` and `check_out` carry no time and no offset, and must never be converted to UTC. The only true instants in the payload are the cancellation deadlines in [Cancellation](#cancellation) and the timestamps in [Payment](#payment) and [Refund](#refund--from-the-moment-one-is-opened).
</Note>

### `booking`

| Field             | Type           | Notes                                                                                                                                                           |
| ----------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `reference`       | string         | The hotel's confirmation number. `""` until issued.                                                                                                             |
| `change_eligible` | bool           | Whether the stay can still be changed online. `false` for non-refundable rates, for suppliers with no modify API, and for any stay that is no longer confirmed. |
| `voucher_url`     | string         | Public link to the confirmation PDF. No authentication — the URL carries a signed token. The same link the guest receives by email. `""` until confirmed.       |
| `property`        | object         | The hotel. See [`property`](#property).                                                                                                                         |
| `room`            | object         | The booked room. See [`room`](#room).                                                                                                                           |
| `guests`          | list           | One entry per booked room. See [Guest](#guest).                                                                                                                 |
| `adults`          | int            | Adults across all rooms                                                                                                                                         |
| `children_ages`   | list of int    | Ages of every child, all rooms concatenated                                                                                                                     |
| `special_request` | string         | Free text passed to the hotel. Never a guarantee.                                                                                                               |
| `value_adds`      | list of string | Rate-level labels, e.g. `["Free WiFi", "Breakfast included"]`                                                                                                   |
| `inclusions`      | list           | Structured rate inclusions. See [Inclusion](#inclusion).                                                                                                        |
| `cancellation`    | object         | See [Cancellation](#cancellation).                                                                                                                              |

The hotel description and the free-text policies — check-in instructions, resort fees, know-before-you-go, pets and children rules — are deliberately not in the event. They are long, HTML-ish, and never fit a card. The voucher PDF carries them.

### `property`

| Field                             | Type           | Notes                                                                |
| --------------------------------- | -------------- | -------------------------------------------------------------------- |
| `name`                            | string         | Hotel name as booked                                                 |
| `star_rating`                     | string         | Decimal string `"5.0"`, or `""` when the supplier reports none       |
| `city`                            | string         |                                                                      |
| `country_code`                    | string         | ISO 3166-1 alpha-2                                                   |
| `country`                         | string         | Resolved full name, so no host-app country table is required         |
| `address`                         | string         | One-line postal address                                              |
| `latitude`, `longitude`           | decimal string | For the map pin. `""` for bookings made before the snapshot existed. |
| `phone`, `email`                  | string         | Hotel contact. `""` when the supplier gives none.                    |
| `check_in_time`, `check_out_time` | string         | The hotel's own local clock, normalised to `HH:MM`                   |
| `images`                          | list           | `{url, caption, category}`. The first element is the cover image.    |

### `room`

| Field                   | Type   | Notes                                                                                                                           |
| ----------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `name`                  | string | Room name as booked, e.g. `Superior Room, 1 King Bed, City View`                                                                |
| `board_type`            | string | `none`, `breakfast`, `lunch`, `dinner`, `half_board`, `full_board`, `all_inclusive`                                             |
| `board_label`           | string | Display string for the above, e.g. `Breakfast included`                                                                         |
| `bed_group_description` | string | The supplier's phrasing of the arrangement the guest picked, e.g. `1 King Bed`. `""` when the guest was never offered a choice. |
| `beds`                  | list   | `{bed_type, count, size}` — one arrangement, never a merge of the alternatives the room offered                                 |
| `rooms_count`           | int    | Rooms booked under this one reservation                                                                                         |
| `images`                | list   | Room-level gallery, same shape as [`property`](#property). Empty for suppliers with no per-room content.                        |
| `amenities`             | list   | `{id, name, category}`. Empty for suppliers with no per-room content.                                                           |

### Guest

One entry per booked room.

| Field                       | Type        | Notes                             |
| --------------------------- | ----------- | --------------------------------- |
| `given_name`, `family_name` | string      | The name that room is held under  |
| `adults`                    | int         | Adults in that room               |
| `children_ages`             | list of int | Ages of the children in that room |

Naming a representative per room is optional at checkout. A room booked without its own guest falls back to the payer, which mirrors what the supplier was sent.

### Inclusion

| Field          | Type   | Notes                    |
| -------------- | ------ | ------------------------ |
| `id`           | string | Supplier's inclusion id  |
| `category`     | string | e.g. `food_and_beverage` |
| `label`        | string | e.g. `Free breakfast`    |
| `offer_type`   | string | e.g. `free`              |
| `frequency`    | string | e.g. `per_day`           |
| `person_count` | int    |                          |

### `cancellation`

| Field                       | Type   | Notes                                                                                                                                                                                         |
| --------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `refundability`             | string | `refundable` \| `non_refundable` \| `partial` \| `unknown`                                                                                                                                    |
| `free_before`               | string | ISO-8601 instant **with offset** for the free-cancellation deadline, or `""`. This one really is a moment — unlike `check_in`.                                                                |
| `summary`                   | string | What the penalty rows cannot say on their own — that the rate does not refund at all, or which stay dates are never refundable. Empty when the rows say everything, which is the common case. |
| `penalties`                 | list   | One window per charge step. See below.                                                                                                                                                        |
| `nonrefundable_date_ranges` | list   | `{start, end, label}` — stay dates that are never refundable                                                                                                                                  |

Each penalty window carries its raw values *and* the two display strings the voucher prints:

| Field                | Notes                                                                                              |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| `start`, `end`       | ISO-8601 instants with offset. `start` is `null` on the first window, `end` is `null` on the last. |
| `amount`, `currency` | The charge, when priced as money                                                                   |
| `nights`, `percent`  | The charge, when priced as nights or a percentage. Unused fields are `null`.                       |
| `label`              | Rendered charge, e.g. `AED 4,025.00`, `1 night`, `Free`                                            |
| `period`             | Rendered window, e.g. `8 Sep 2026, 15:00 – 10 Sep 2026, 15:00`                                     |

<Warning>
  **Render `label` and `period`; do not re-format the numbers.** They are produced by the same renderer the voucher and the miniapp use, so re-wording them is how the app and the document come to describe one booking differently.
</Warning>

### `price`

| Field                  | Type           | Notes                                                                                                                                     |
| ---------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `currency`             | string         | Same as `total_currency`                                                                                                                  |
| `base_amount`          | decimal string | Room rate before tax                                                                                                                      |
| `tax_amount`           | decimal string | Taxes and service fees taken with the charge                                                                                              |
| `fees_amount`          | decimal string | Property and resort fees                                                                                                                  |
| `total_amount`         | decimal string | Same as the top-level `total_amount`                                                                                                      |
| `due_now`              | decimal string | The part collected at checkout                                                                                                            |
| `due_at_property`      | decimal string | What the guest still pays at the desk. **Often non-zero on a fully paid stay** — city tax, resort fee — and never part of `total_amount`. |
| `strikethrough_amount` | decimal string | Pre-discount price; `""` when the rate carried no discount                                                                                |
| `discount_percent`     | decimal string | `""` when none                                                                                                                            |
| `merchant_of_record`   | string         | Who takes the money at the hotel end                                                                                                      |
| `nightly_rates`        | list           | `{night, base, taxes_and_fees, total, currency}`, one per night                                                                           |

A charge the hotel bills in its own currency is never folded into these aggregates: converting it would misstate it. There is no currency conversion anywhere in this flow — a stay is quoted, charged and refunded in one currency.

### `payment`

| Field             | Type   | Notes                                                                                         |
| ----------------- | ------ | --------------------------------------------------------------------------------------------- |
| `method`          | string | `host_wallet` when the host app charged its own wallet; `crypto` when the guest paid on-chain |
| `asset`, `amount` | string | The crypto asset and amount. Both `""` for `host_wallet`.                                     |
| `exchange_rate`   | string | Order currency per unit of asset, frozen at payment-create time. `""` for `host_wallet`.      |
| `tx_hash`         | string | Incoming on-chain transaction. `""` for `host_wallet`.                                        |
| `paid_at`         | string | UTC instant the payment was confirmed                                                         |

### `refund` — from the moment one is opened

| Field         | Type           | Notes                                                                                                            |
| ------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `kind`        | string         | `full` \| `partial` \| `none` \| `manual`. `manual` means the amount is an operator's call, not a computed one.  |
| `amount`      | decimal string | Net amount to return to the guest. `"0.00"` when the policy returns nothing.                                     |
| `currency`    | string         |                                                                                                                  |
| `reason`      | string         | See [The five event types](#the-five-event-types)                                                                |
| `status`      | string         | `in_progress` \| `refunded` \| `needs_review`. `needs_review` means a human must confirm before any money moves. |
| `tx_hash`     | string         | Filled on `order.refunded` for an on-chain refund; `""` otherwise                                                |
| `refunded_at` | string         | Filled on `order.refunded`; `""` before                                                                          |

Refunds are never issued automatically for an outcome that cannot be proven failed. A supplier timeout is reconciled first, because refunding a stay that actually confirmed is worse than refunding late.

### `change` — on `booking.changed` only

| Field                                     | Type           | Notes                                                                                         |
| ----------------------------------------- | -------------- | --------------------------------------------------------------------------------------------- |
| `previous_check_in`, `previous_check_out` | string         | The stay as it was before the change                                                          |
| `previous_total_amount`                   | decimal string |                                                                                               |
| `delta_amount`, `delta_currency`          | string         | Extra charged for the change. A cheaper change is refunded, not reported as a negative delta. |
| `delta_payment_id`                        | string         | The second order payment that funded the change                                               |
| `applied_at`                              | string         | UTC instant the supplier committed the change                                                 |

## Delivery, retries, and idempotency

* Events are sent **after** the corresponding state change is committed, so delivered payloads describe committed state.
* A failed delivery is retried periodically, up to **5 attempts** in total. After that it is flagged for operational review and can be re-sent once the host-app endpoint is healthy.
* Retried events are **re-derived from current order state**. If the order changes between attempts, the retry describes the current state rather than replaying stale data — and its `event_type` is re-derived with it, so a cancellation that has since been refunded arrives as `order.refunded`.
* Delivery failures never affect the booking. An unavailable host-app endpoint cannot prevent a reservation.
* Any response other than HTTP `200` counts as a failure and is retried. Unsupported event types must also receive `200`.

<Warning>
  **The receiver must be idempotent.** The same event can arrive more than once, and events for one order can arrive out of order after an outage. Key records on `miniapp_order_id` and treat each payload as current order state rather than as a delta. Compare `payment_status` and `fulfillment_status` with stored values to discard stale repeats.
</Warning>

This is also what makes the second `order.succeeded` free: it is the same order restated with the hotel's confirmation number filled in.

## Reading `status`

`status` is a one-word summary of two independent facts: whether payment settled and whether a room exists. All lifecycle events are emitted only after `payment_status=PAID`; the three status fields still remain in the payload for explicit state tracking.

| `payment_status` | `fulfillment_status` | `status`      | Lifecycle event                                                                 |
| ---------------- | -------------------- | ------------- | ------------------------------------------------------------------------------- |
| `PAID`           | `COMPLETED`          | `BOOKED`      | `order.succeeded`; `booking.changed` for a confirmed stay update                |
| `PAID`           | `FAILED`             | `FAILED`      | `order.failed`, followed by `order.refunded` when a refund obligation is opened |
| `PAID`           | `CANCELLED`          | `CANCELLED`   | `booking.cancelled`; may be followed by `order.refunded`                        |
| `PAID`           | `PROCESSING`         | `BOOKING`     | none — the supplier has not answered yet                                        |
| `PAID`           | `PENDING`            | `BOOKING`     | none — payment settled but booking has not started                              |
| `REFUNDED`       | `CANCELLED`          | `REFUNDED`    | `order.refunded`                                                                |
| `PENDING`        | any                  | derived state | none — payment has not settled                                                  |
| `FAILED`         | any                  | derived state | none — the host app declined or failed the payment                              |

<Note>
  The critical combination is **`payment_status=PAID` with `fulfillment_status=FAILED`**: the guest was charged but holds no reservation. It produces `order.failed`, followed by `order.refunded`.
</Note>

A finished stay is not a status. Check-out is recorded a day after the fact and deliberately leaves the order `BOOKED`, so no event is sent when a guest goes home.

## Full example — `order.succeeded`

As delivered to the host-app event receiver. Captured from a live booking.

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "client_id": "host_XXXXXXXXXXXX",
  "event_type": "order.succeeded",
  "payload": {
    "miniapp_order_id": "08fa8e4d-6633-469e-b6be-8f3f926f8f34",
    "booking_id": "af523b33-4c3a-496c-9563-27ee92f01081",
    "status": "BOOKED",
    "payment_status": "PAID",
    "fulfillment_status": "COMPLETED",
    "booking_reference": "880928193161802",
    "total_amount": "922.86",
    "total_currency": "USD",
    "destination": "London, GB",
    "check_in": "2026-10-21",
    "check_out": "2026-10-24",
    "nights": 3,
    "payment_error_code": "",
    "booking": {
      "reference": "880928193161802",
      "change_eligible": true,
      "voucher_url": "https://api.boxo.io/api/v1/hotels/vouchers/ImFmNTIz…/",
      "property": {
        "name": "Novotel London West",
        "star_rating": "4.0",
        "city": "London",
        "country_code": "GB",
        "country": "United Kingdom",
        "address": "1 Shortlands, Hammersmith, London, England, W6 8DR",
        "latitude": "51.492013",
        "longitude": "-0.219884",
        "phone": "44-020-8741 1555",
        "email": "",
        "check_in_time": "15:00",
        "check_out_time": "12:00",
        "images": [
          {
            "url": "https://i.travelapi.com/lodging/…/0ffdeff0_z.jpg",
            "caption": "Front of property",
            "category": "exterior"
          }
        ]
      },
      "room": {
        "name": "Superior Room, 1 Double Bed with Sofa bed",
        "board_type": "none",
        "board_label": "Room only",
        "bed_group_description": "",
        "beds": [{ "bed_type": "double", "count": 1, "size": "Full" }],
        "rooms_count": 1,
        "images": [],
        "amenities": [
          { "id": "1073743284", "name": "Heating", "category": "" }
        ]
      },
      "guests": [
        {
          "given_name": "Aisha",
          "family_name": "Mansoori",
          "adults": 2,
          "children_ages": [7]
        }
      ],
      "adults": 2,
      "children_ages": [7],
      "special_request": "",
      "value_adds": [],
      "inclusions": [],
      "cancellation": {
        "refundability": "refundable",
        "free_before": "2026-10-20T23:59:00-08:00",
        "summary": "",
        "penalties": [
          {
            "start": null,
            "end": "2026-10-20T23:59:00-08:00",
            "amount": "0",
            "currency": null,
            "nights": null,
            "percent": null,
            "description": "",
            "label": "Free",
            "period": "Until 20 Oct 2026, 11:59 PM"
          },
          {
            "start": "2026-10-20T23:59:00-08:00",
            "end": "2026-10-21T23:59:00-08:00",
            "amount": "0",
            "currency": "USD",
            "nights": 1,
            "percent": null,
            "description": "",
            "label": "1 night",
            "period": "20 Oct 2026, 11:59 PM – 21 Oct 2026, 11:59 PM"
          }
        ],
        "nonrefundable_date_ranges": []
      }
    },
    "price": {
      "currency": "USD",
      "base_amount": "769.05",
      "tax_amount": "153.81",
      "fees_amount": "",
      "total_amount": "922.86",
      "due_now": "922.86",
      "due_at_property": "",
      "strikethrough_amount": "",
      "discount_percent": "",
      "merchant_of_record": "",
      "nightly_rates": [
        {
          "night": 1,
          "base": "327.04",
          "taxes_and_fees": "65.41",
          "total": "392.45",
          "currency": "USD"
        }
      ]
    },
    "payment": {
      "method": "host_wallet",
      "asset": "",
      "amount": "",
      "exchange_rate": "",
      "tx_hash": "",
      "paid_at": "2026-10-20T20:13:34+00:00"
    },
    "refund": null,
    "change": null
  }
}
```

On `order.refunded`, `payload.refund` carries:

```json theme={"system"}
{
  "kind": "full",
  "amount": "922.86",
  "currency": "USD",
  "reason": "booking_cancelled",
  "status": "refunded",
  "tx_hash": "",
  "refunded_at": "2026-10-20T20:17:46+00:00"
}
```

## Reading an order without storing every event

The hotels miniapp does **not** yet answer the platform's `booking.info` request, which is how the [flights miniapp](/mini-apps/flights/integration#request-booking-information----bookinginfo) backfills a host app after an outage. Until it does, two authenticated endpoints serve the same purpose.

**`GET /api/v1/hotels/payments/{payment_id}/`** — keyed on `miniapp_order_id`, which the host app already holds. `checkout_state` collapses the several payment columns into one word:

| `checkout_state`                     | Meaning                                   |
| ------------------------------------ | ----------------------------------------- |
| `awaiting_payment`                   | Issued, not yet charged                   |
| `confirming`                         | Charge reported, being confirmed          |
| `booking`                            | Paid; the room is being secured           |
| `booked`                             | Confirmed. `hotel_booking_id` is set.     |
| `booking_failed_refunding`           | Charged, no room, refund in flight        |
| `booking_failed_support`             | Outcome ambiguous; a human is settling it |
| `refunded`                           | Money returned                            |
| `payment_expired` / `payment_failed` | Never charged                             |

**`GET /api/v1/hotels/bookings/{booking_id}/`** — the full booking. **`GET /api/v1/hotels/bookings/`** — the guest's bookings, for a nightly reconciliation.

## Payments

Hotel checkout uses the standard [Boxo Payments](/host-apps/BoxoPayments) flow, with the same two host-app integration points the flights miniapp uses.

### Create order payment request

After a successful price check, the hotels miniapp asks the Boxo Platform to create the payment, and the platform calls the host app's **create order payment** endpoint. The body carries `order.miniapp_order_id`, which is **the same id included in every lifecycle event for that order** and should be stored with the payment.

```json theme={"system"}
{
  "app_id": "app_XXXXXX",
  "client_id": "host_XXXXXXXXXXXX",
  "order": {
    "currency": "USD",
    "amount": "922.86",
    "subtotal_amount": "769.05",
    "shipping_amount": "0",
    "discount_amount": "100.00",
    "tax_title": "Taxes and fees",
    "tax_amount": "153.81",
    "taxes_included": true,
    "note": "",
    "miniapp_order_id": "08fa8e4d-6633-469e-b6be-8f3f926f8f34",
    "hostapp_user_id": "host-user-1",
    "custom_attributes": {
      "order_type": "BOOKING",
      "property_name": "Novotel London West",
      "location": { "city": "London", "country_code": "GB" },
      "check_in": "2026-10-21",
      "check_out": "2026-10-24",
      "nights": 3,
      "room_name": "Superior Room, 1 King Bed",
      "board": "Breakfast included",
      "rooms_count": 1,
      "adults": 2,
      "children": 1,
      "refundability": "refundable",
      "free_cancellation_before": "2026-10-20T23:59:00-08:00"
    }
  }
}
```

Return `{"order_payment_id": "..."}` with HTTP `200`. The guest settles that payment through the SDK.

| Field               | Meaning                                                                             |
| ------------------- | ----------------------------------------------------------------------------------- |
| `amount`            | What the guest is charged. Tax is included in it.                                   |
| `subtotal_amount`   | The room rate before tax                                                            |
| `tax_amount`        | Taxes and service fees taken with the charge                                        |
| `discount_amount`   | The difference from the pre-discount price; `"0"` when the rate carried no discount |
| `taxes_included`    | Always `true` — `amount` is the whole of what the guest pays                        |
| `miniapp_order_id`  | The order key. Store it with the payment; every event carries it.                   |
| `hostapp_user_id`   | The guest's `reference` from Boxo Connect                                           |
| `custom_attributes` | What is being bought, in hotel terms — see below                                    |

### Payment custom attributes

The order fields above carry an amount and little else, which leaves a payment sheet, a statement line or a support screen with nothing but a number on it. Every Boxo miniapp fills `custom_attributes` with its own product's shape — the [eSIM](/mini-apps/esim/integration#payment-payload) one sends the plan, its coverage and its data allowance — and this is the hotel equivalent.

| Key                        | Type   | Notes                                                                                                                                                      |
| -------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `order_type`               | string | `BOOKING` for a stay. `CHANGE` is reserved for the extra charge on a date change; that flow does not yet issue host-app payments, so it is not sent today. |
| `property_name`            | string | The hotel as it will appear on the booking                                                                                                                 |
| `location`                 | object | `{city, country_code}`                                                                                                                                     |
| `check_in`, `check_out`    | string | Calendar dates at the property, `YYYY-MM-DD`                                                                                                               |
| `nights`                   | int    | Length of stay                                                                                                                                             |
| `room_name`                | string | The room as the guest chose it                                                                                                                             |
| `board`                    | string | Meal plan, already phrased — `Room only`, `Breakfast included`, …                                                                                          |
| `rooms_count`              | int    | Rooms under this one reservation                                                                                                                           |
| `adults`                   | int    | Adults across all rooms                                                                                                                                    |
| `children`                 | int    | Children across all rooms. Absent when there are none.                                                                                                     |
| `refundability`            | string | `refundable` \| `non_refundable` \| `partial` \| `unknown`                                                                                                 |
| `free_cancellation_before` | string | ISO-8601 instant with offset for the free-cancellation deadline                                                                                            |

<Note>
  **Keys with nothing behind them are left out, not sent empty.** A payment sheet reading "Room: —" is worse than one with no such line, so a host app should treat every key here as optional and render only what arrived. `order_type` is the one that is always present.
</Note>

<Note>
  **Our net cost is deliberately not here.** The eSIM payload carries `provider_net_price`; hotel net rates are commercially sensitive in a way plan costs are not, so sending them to a host app is a business decision rather than a technical default. Ask if it is wanted.
</Note>

<Note>
  **What is not in `amount`.** A hotel collects some charges at the desk — city tax, resort fee — and those are never part of the figure the host app charges. They travel on the order events as `price.due_at_property` so the host app can show them, clearly separated from what was taken.
</Note>

The breakdown comes from the price-checked snapshot, not from anything the client asserted, and `amount` is verified against it to the cent before the request is made.

`subtotal_amount` and `tax_amount` always add up to `amount`. Some suppliers report the two separately and some report only their sum, in which case the room rate is derived — so the figures are consistent either way, but `subtotal_amount` is not always a number the supplier itself quoted. Where a rate carries no split at all, the whole amount is sent as the subtotal and `tax_amount` is `"0"`, rather than a tax figure being invented.

### Report the payment result

After the guest confirms, call the hotels backend:

```bash theme={"system"}
curl --location --request POST \
  'https://[HOTELS_URL]/api/v1/hotels/payments/payment-notify/{{MINIAPP_SLUG}}/' \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Basic {{BASE64_APP_ID_AND_SECRET_KEY}}' \
  --header 'X-Miniapp-App-Id: {{APP_ID}}' \
  --header 'X-Hostapp-Client-Id: {{CLIENT_ID}}' \
  --data-raw '{
    "hostapp_order_payment_id": "{{ORDER_PAYMENT_ID}}",
    "payment_status": "paid"
  }'
```

How each status affects the order:

| `payment_status`      | Effect                                                                                                                  |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `paid`                | The order is marked `PAID` and booking proceeds. A lifecycle event is emitted only after this point.                    |
| `cancelled`, `failed` | The order closes as unpaid. **No lifecycle event is emitted.** Include `payment_fail_reason` for operational diagnosis. |
| `in_process`          | No state change; the order remains pending.                                                                             |

**This endpoint always answers HTTP `200`**, with any problem reported in the body:

```json theme={"system"}
{"code": "SUCCESS"}
{"error_code": "PAYMENT_NOT_FOUND"}
{"error_code": "INVALID_ACCESS_TOKEN"}
{"error_code": "PAYMENT_NOT_CONFIRMABLE"}
```

A retry policy should therefore key on the body, not the status line. The call is idempotent: a repeat for a payment already recorded as paid answers `SUCCESS` and does nothing. A `failed` notice for a payment already settled is ignored as an out-of-order redelivery.

Booking starts **after** the payment result is acknowledged, not during the request. `order.succeeded`, rather than the response to this call, is the signal that a room exists.

### Get order payment status

If a payment result is not delivered, the hotels miniapp asks the Boxo Platform, which calls the host app's **get order payment status** endpoint. It must return the current status (`in_process`, `paid`, `cancelled`, or `failed`).

This is the recovery path for a lost payment notification, and it runs on its own every few minutes for the whole life of a payment window. A missing or incorrect implementation can leave a charged guest without a room or a refund — a `paid` answer here takes exactly the same path the webhook would have taken, booking included.

### Booking before funds is not an option

The supplier bills Boxo the moment a stay confirms, so the room is never secured before payment-notify or get-order-payment-status reports a settled payment. Money then sits in escrow until the booking actually succeeds — a booking that fails never had its funds released, which is what makes the refund a clean reversal rather than a recovery.

## Guest identity

Login uses [Boxo Connect](/host-apps/BoxoConnect) through either the OAuth or Direct flow. In both flows, **`reference`** in the supplied user data is the relevant identity field.

`reference` becomes the guest's identity in the hotels miniapp.

<Warning>
  It must be **stable for a user across sessions and devices**. If it changes, that guest's booking history goes with it.
</Warning>

Supplying `first_name`, `last_name`, `email`, and `phone` allows checkout to prefill guest details.

## Integration checklist

<Steps>
  <Step title="Enable Event Bridge">
    Request that Boxo enable **Event Bridge** for the host app and hotels miniapp, and register the host-app **event receiver URL**.
  </Step>

  <Step title="Confirm Authorization scheme">
    Confirm whether the receiver expects `Basic` or `Token` on the Authorization header.
  </Step>

  <Step title="Verify inbound credentials">
    Verify `hostapp_client_id:hostapp_secret_key` on every inbound call.
  </Step>

  <Step title="Acknowledge every event">
    Return `200` from the event receiver for **every** event, including unsupported types.
  </Step>

  <Step title="Route on event_type">
    Route on `event_type`. Do not reduce the five types to one generic status update.
  </Step>

  <Step title="Key records on miniapp_order_id">
    Key records on `payload.miniapp_order_id` and make handlers idempotent.
  </Step>

  <Step title="Expect a second order.succeeded">
    Expect `order.succeeded` **twice** for some orders — the second carries the hotel's confirmation number. Overwrite, do not duplicate.
  </Step>

  <Step title="Render booking cards from the payload">
    Render booking cards from `payload.booking`: hotel name, images, room, beds, guests, and the cancellation `label` / `period` strings as given. No host-app lookup tables are required.
  </Step>

  <Step title="Treat booking.changed as an update">
    Treat `booking.changed` as an update to an existing booking, never as a new one.
  </Step>

  <Step title="Refund the net amount">
    On `order.refunded`, return `refund.amount` — already net — against the order payment the host app settled.
  </Step>

  <Step title="Read payment and fulfillment together">
    Read `payment_status` and `fulfillment_status` together, not just `status`.
  </Step>

  <Step title="Call payment-notify after every charge">
    Call `payment-notify` after every charge, and key the retry on the response body rather than the status line.
  </Step>

  <Step title="Keep Connect reference stable">
    Keep `reference` stable per user at connect.
  </Step>
</Steps>

## Quick reference

| Direction                 | What                | Body                                                   | Where                          |
| ------------------------- | ------------------- | ------------------------------------------------------ | ------------------------------ |
| Hotels miniapp → host app | `order.succeeded`   | envelope + [payload](#payload-reference)               | Host-app event receiver        |
| Hotels miniapp → host app | `order.failed`      | envelope + [payload](#payload-reference)               | Host-app event receiver        |
| Hotels miniapp → host app | `order.refunded`    | envelope + [payload](#payload-reference) with `refund` | Host-app event receiver        |
| Hotels miniapp → host app | `booking.changed`   | envelope + [payload](#payload-reference) with `change` | Host-app event receiver        |
| Hotels miniapp → host app | `booking.cancelled` | envelope + [payload](#payload-reference)               | Host-app event receiver        |
| Host app → hotels miniapp | Payment result      | `{hostapp_order_payment_id, payment_status}`           | `POST` payment-notify          |
| Host app → hotels miniapp | Read an order       | —                                                      | `GET` payments/\{payment\_id}/ |
| Host app → hotels miniapp | Read a booking      | —                                                      | `GET` bookings/\{booking\_id}/ |

Platform reference: [Boxo Event Bridge](/host-apps/CES), [Boxo Payments](/host-apps/BoxoPayments), [Boxo Connect](/host-apps/BoxoConnect).
