Skip to main content
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: 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. 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.
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.
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

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.

Dashboard settings to confirm

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

Authentication

Calls received by the host app (event receiver) carry:
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:
IP whitelisting and request signaturing are also available for securing server-to-server calls. See Security measures.

What an event delivery looks like

Every hotel order event arrives at the host app’s event receiver in the platform’s standard envelope:
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.
The receiver may also receive event types from other miniapps and the platform. Unsupported event types should be acknowledged with 200 and ignored.

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. 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.
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.
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.
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.
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. 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.

Top level

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 and the timestamps in Payment and Refund.

booking

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

room

Guest

One entry per booked 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

cancellation

Each penalty window carries its raw values and the two display strings the voucher prints:
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.

price

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

refund — from the moment one is opened

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

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.
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.
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.
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.
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.
On order.refunded, payload.refund carries:

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 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: 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 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.
Return {"order_payment_id": "..."} with HTTP 200. The guest settles that payment through the SDK.

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 one sends the plan, its coverage and its data allowance — and this is the hotel equivalent.
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.
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.
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.
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:
How each status affects the order: This endpoint always answers HTTP 200, with any problem reported in the body:
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 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.
It must be stable for a user across sessions and devices. If it changes, that guest’s booking history goes with it.
Supplying first_name, last_name, email, and phone allows checkout to prefill guest details.

Integration checklist

1

Enable Event Bridge

Request that Boxo enable Event Bridge for the host app and hotels miniapp, and register the host-app event receiver URL.
2

Confirm Authorization scheme

Confirm whether the receiver expects Basic or Token on the Authorization header.
3

Verify inbound credentials

Verify hostapp_client_id:hostapp_secret_key on every inbound call.
4

Acknowledge every event

Return 200 from the event receiver for every event, including unsupported types.
5

Route on event_type

Route on event_type. Do not reduce the five types to one generic status update.
6

Key records on miniapp_order_id

Key records on payload.miniapp_order_id and make handlers idempotent.
7

Expect a second order.succeeded

Expect order.succeeded twice for some orders — the second carries the hotel’s confirmation number. Overwrite, do not duplicate.
8

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.
9

Treat booking.changed as an update

Treat booking.changed as an update to an existing booking, never as a new one.
10

Refund the net amount

On order.refunded, return refund.amount — already net — against the order payment the host app settled.
11

Read payment and fulfillment together

Read payment_status and fulfillment_status together, not just status.
12

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.
13

Keep Connect reference stable

Keep reference stable per user at connect.

Quick reference

Platform reference: Boxo Event Bridge, Boxo Payments, Boxo Connect.