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 Basicbase64(hostapp_client_id:hostapp_secret_key), using the host-app credentials configured in the dashboard.
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: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 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.
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.
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
Thepayload 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:
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_typeis re-derived with it, so a cancellation that has since been refunded arrives asorder.refunded. - Delivery failures never affect the booking. An unavailable host-app endpoint cannot prevent a reservation.
- Any response other than HTTP
200counts as a failure and is retried. Unsupported event types must also receive200.
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.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.
order.refunded, payload.refund carries:
Reading an order without storing every event
The hotels miniapp does not yet answer the platform’sbooking.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 carriesorder.miniapp_order_id, which is the same id included in every lifecycle event for that order and should be stored with the payment.
{"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 fillscustom_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.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:
This endpoint always answers HTTP
200, with any problem reported in the body:
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.
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.