Skip to main content
POST
Create a booking only after a successful Prebook a Room response whose revalidation.decision is READY_TO_BOOK, or is RECONFIRMATION_REQUIRED and the guest accepted the change. You send the guests for every room, their answers to the offer’s booking_requirements, and an Idempotency-Key. The response is a booking in PROCESSING or AWAITING_REVIEW. It is never an instant confirmation. Track the result with Get Booking Status or webhooks. The flow: search, rooms, prebook, create booking, track status, show voucher.

Request example

Request field guide

Headers

Body

There is no separate field for a contact, a client reference or special requests. The guest contact is the lead guest’s EMAIL and PHONE answers when the offer asks for them. Use the Idempotency-Key as your reference: it is stored with the booking and you can look the booking up with it (see Get Booking by Idempotency Key). Keep your own order number next to the returned booking_id in your database.

Building the room assignments

For every room in the prebook response:
  1. Copy its room_allocation_id into one booking room object.
  2. Read the room’s occupancy.adults and occupancy.child_ages.
  3. Add one ADULT guest per adult.
  4. Add one minor per entry in child_ages. A child aged 0 or 1 is an INFANT; age 2 and over is a CHILD.
  5. Repeat until every prebook room appears once.
An occupancy of {"adults": 1, "child_ages": [8]} needs one ADULT and one CHILD. The child’s age is already known from the search, so the guest object supplies identity only. If you also send a DATE_OF_BIRTH answer for a child, it must give the quoted age on the check-in date. A room with mapping_status: "UNMAPPED" is booked through its room_allocation_id in exactly the same way as a mapped room.

Answering booking_requirements

The prebook offer’s booking_requirements tell you which data to collect. Treat them as the form definition:
  • Send { "requirement_id": "...", "value": "..." } and copy the id exactly. Never build ids from code.
  • scope says who answers: ALL_GUESTS, LEAD_GUEST (only the guest with is_lead_guest: true), or ADULT, CHILD, INFANT (by guest_type). When room_allocation_ids is not empty, only guests in those rooms answer.
  • Answer every requirement with required: true that applies to the guest. Missing answers return VALIDATION_ERROR listing what is missing.
  • Do not answer requirements that do not apply to the guest, answer one twice, or send an id the offer did not declare. These return VALIDATION_ERROR.
  • Values are checked against the requirement: an EMAIL must be an email, a PHONE must be in international format with a leading + (for example +919876543210), a DATE must be YYYY-MM-DD, a COUNTRY_CODE must be an uppercase two-letter code, an ENUM must be one of allowed_values, and constraints (length, pattern, age range) apply. Values cannot be empty.
  • Titles are MR, MRS, MS, MISS, MASTER or OTHER.
  • Which fields exist depends on the supplier. First name, last name and title are asked for every guest. Email and phone appear only when the supplier needs them, so do not assume a lead-guest contact is required. Passport or tax-ID requirements, when present, apply to every guest. Names for suppliers that limit characters (for example RateHawk: letters, spaces and ' - , . ’ only) carry a constraints.pattern; a breach returns GUEST_NAME_INVALID (422).
  • Some suppliers need a booking contact saved in your portal settings. If it is missing the request fails with PROVIDER_CONFIGURATION_ERROR (422).

Order of rooms and guests

The rooms array can be sent in any order. Unifystays matches each entry by room_allocation_id and restores the supplier’s trusted room order itself.

What happens next

Create Booking answers 200 with a booking in one of two starting states: Neither status is a confirmed reservation. A PROCESSING booking that is queued goes to the supplier as it is; it never moves to AWAITING_REVIEW afterwards. An organization owner can turn on Require approval for non-refundable bookings in the portal under Settings, Organization. It is off by default. Bookings that are non-refundable or whose free cancellation ends within 2 days are then held for approval, as described below.

Example: queued for the supplier

Example: awaiting approval

Booking statuses and what to do

Webhook events for bookings are booking.created, booking.confirmed, booking.failed, booking.cancellation_requested, booking.cancelled, booking.cancellation_failed and booking.updated (other status changes such as PROCESSING to ON_HOLD, and new supplier references). Webhooks can arrive late or more than once, so make your handler safe to repeat and keep polling as a fallback. See Webhooks.
If the status is PROCESSING with processing_reason: "RECONCILIATION_REQUIRED" and CONTACT_SUPPORT in available_actions, the supplier’s answer was uncertain. Do not book again; contact support with the booking_id.

Approval for bookings outside free cancellation

This applies only to organizations that require approval for non-refundable bookings. For AWAITING_REVIEW, show “Awaiting verification, room and price are not reserved”. An organization owner or admin reviews the saved hotel, rooms, guests, price and cancellation terms in the portal and chooses Approve and book. REVIEW_BOOKING is a portal action; API keys cannot approve.
  • The booking reference and the price you agreed stay the same. If the supplier quote has expired, the reviewer obtains a fresh quote in the portal and accepts any supplier cost difference.
  • data.review_expires_at is the quote deadline. Manual requests stay AWAITING_REVIEW after it passes; do not cancel or refund because of this timestamp. Approval and supplier submission must happen before the fresh quote expires.
  • Rejection ends in FAILED with failure.code BOOKING_REVIEW_REJECTED. Expiry while a free-cancellation booking waits for submission ends in FAILED with BOOKING_REVIEW_EXPIRED, before any reservation is submitted. A finished request cannot be refreshed.
  • A request that is still AWAITING_REVIEW is closed automatically as FAILED with BOOKING_REVIEW_EXPIRED (and a booking.failed webhook) when its quote expired more than 24 hours ago, when the check-in date has arrived, or when you already have a CONFIRMED booking for the same hotel, dates, lead guest and number of rooms. Nothing was reserved, so no refund is needed beyond your usual handling of failed bookings.

Hold bookings

Send "booking_mode": "HOLD" only if prebook lifecycle.available_booking_modes contains HOLD (lifecycle.hold_allowed is true). Otherwise the request fails with HOLD_NOT_ALLOWED (409). The booking is reserved with the supplier but not final: it goes PROCESSING, then ON_HOLD, with hold_deadline_at set. It is announced by a booking.updated webhook. Then confirm it or let it lapse. Details are on Confirm Hold. A hold never waits for review: it costs nothing until confirmed, so it goes to the supplier straight away however close its deadline is. If the hold deadline has already passed, the request fails with HOLD_DEADLINE_EXPIRED (409).

Idempotency and safe retries

Generate one unique key per customer booking attempt. Save it before the first request. On a timeout or an unreadable response, retry with the same key and the same request body.
  • A repeat with the same key and the same body returns the original booking (200, same booking_id). It never creates a second reservation, even after the prebooking_id has been used. Array order and answer order do not change the match.
  • The same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED.
  • Keys are private to your organization and may be up to 128 characters. A missing key returns IDEMPOTENCY_KEY_REQUIRED.
  • If you never received the response, ask Get Booking by Idempotency Key. 404 means no booking was created, so it is safe to resend with the same key.
  • A request that fails validation creates nothing and does not consume the prebooking_id. Correct the body and resend (same key is fine).
  • Use a new key only when the customer intentionally starts a new attempt, for example after returning to the rooms and prebooking again. A new key is a new booking.

Errors to handle

The error envelope and request IDs are described in Errors & Retries.
Booking contains guest personal data. Send it only from your trusted backend and avoid writing unprotected guest information to client-side logs.
The OpenAPI section below documents all booking fields and responses.

Authorizations

x-api-key
string
header
required

Environment-specific API key created in the Unifystays portal

Headers

X-Unifystays-End-User-IP
string

Public IPv4 or IPv6 address of the traveler completing checkout. Needed for RateHawk offers: when it is missing or not a public address (private, loopback and reserved ranges are rejected) the request fails with VALIDATION_ERROR. If you omit the header the connection address is used, so send it whenever your server calls on the traveler's behalf.

Idempotency-Key
string
required

Required, max 128 characters. One unique key per booking attempt, saved before you send the request. Resending the same key with the same body (after a timeout, for example) returns the original booking and never books twice. The same key with a different body returns 409 IDEMPOTENCY_KEY_REUSED. Use a new key only for a new attempt.

X-Unifystays-Contract-Version
enum<string>
default:2

Hotel API contract version. Optional: 2 is the current version and the default.

Available options:
2
X-Unifystays-Currency
string

ISO 4217 currency for display amounts (display_ledger, display_amount, commercial_display). Optional: defaults to your organization's display currency, USD unless configured. Read pricing.display_currency in the response.

Required string length: 3
Example:

"EUR"

Body

application/json
prebooking_id
string
required

The data.prebooking_id from the successful Prebook response. It is single use and expires with the prebooked offer (offer.lifecycle.expires_at). If this request fails validation the id is not consumed and you may correct the body and resend.

Example:

"pbq7.bjNQb1Y5R2hRWEg.k6jQ3nW4cF1nL4MWvN7A9Q"

accepted_offer_id
string
required

The data.offer.offer_id from the same Prebook response as prebooking_id. It confirms you are booking the refreshed offer the guest saw and accepted; any other id is rejected with ACCEPTED_OFFER_MISMATCH.

Example:

"off_65db5f20d91b47d3a73d834dfa44c50e"

rooms
object[]
required

One entry per room in the prebook offer's rooms[], each with its guests. The count must equal the number of prebook rooms and array order does not matter.

booking_mode
enum<string>
default:INSTANT

INSTANT (default) books the room straight away. HOLD reserves it until a supplier deadline, and is accepted only when the prebook offer's lifecycle.available_booking_modes contains HOLD; otherwise HOLD_NOT_ALLOWED. A held booking is confirmed later with Confirm Hold.

Available options:
INSTANT,
HOLD

Response

The booking was created or an earlier booking with the same Idempotency-Key was returned. The status is PROCESSING or AWAITING_REVIEW at this point; it is not a confirmation. Track it with Get Booking Status or webhooks.

contract_version
string
required

Response contract version. Always "2".

Example:

"2"

success
boolean
required

false only when the booking status is FAILED. true for every other status, including AWAITING_REVIEW and PROCESSING, which are not confirmations. Read data.status for the real state.

Example:

true

message
string
required

Human-readable summary for logs and support; for FAILED it carries the reason. Do not parse it; use data.status.

Example:

"Booking request accepted and is being processed."

data
object
required

The booking's current state.