Skip to content
yaReserva

Search the site

Go to

Searches the 78 features, the 36 guides, the documentation and the frequently asked questions.

Book

Integration

Your own booking engine

For the hotel that runs on yaReserva but sells from the search box on its own website. Six endpoints: what is available, what it costs, book, look up, cancel and charge.

Who this is for

The hotel already has its website, with its own design and its own search box, and does not want to swap it for ours. Underneath, inventory, prices, taxes and policies are still yaReserva’s. This API is the bridge: your engine asks and books, and the booking shows up in the hotel’s panel like any other.

It is not a second booking engine: it is the same one that drives the Web Embed, with a different screen in front. That is why there are never two truths about the same inventory and no risk of overbooking. The only thing that changes is the channel the sale is recorded under, which is what decides the commission.

If the hotel has nobody to build this, it does not need to: the Web Embed is a ready-made search box you paste into the site with one line. This API is for those who want control over the design and the flow.

Rule: The payment intent is always created by yaReserva

The hotel’s engine never calls Stripe to create the charge. Our backend creates it, the amount comes from the already-sealed booking — not from whatever the browser sends — and the account where the charge is born is decided by the hotel’s configuration, not by a parameter. A payment intent created in the frontend is an amount the buyer picks and an account the buyer points at.

Rule: The secret is shown only once

When the connection is created or rotated, the panel shows the secret once and never again: it is stored encrypted and neither the hotel nor we can read it afterwards. Put it in your secret manager right then. If it is lost, the way out is to rotate, and the previous secret stops working immediately.

Credentials and domains

The hotel creates the connection in its panel, under Settings → Booking engine, and picks the property. There it receives, once and only once, the credential pair.

ValueShapeUse
keyIdyreng_ + 24 hexPublic. Identifies the connection and travels in every request.
secretyreng_sk_ + 64 hexSecret. Signs the requests and never travels inside them.

Besides the key pair, the connection carries two settings the hotel controls from the panel: the authorized domains and the per-booking cap.

Rule: With no authorized domains, the key is server-only

A connection with no declared domains admits none: if the request arrives with an Origin header, it is rejected with 403 ORIGIN_NOT_ALLOWED even when the signature is perfect. Seeing an Origin means there is a browser behind it, and a browser holding that key is a key given away, because the signing secret lives in it. If your engine really does run in the browser, the hotel has to declare its domains first.

  • Up to ten domains per key, with scheme and host: “https://www.mihotel.mx”. They are stored normalized, lowercased and without path or trailing slash.
  • “http://localhost” and “http://localhost:3000” are allowed, so the hotel can develop its engine.
  • The response echoes the concrete Origin, never “*”: with a wildcard any page could read it, which is exactly what the list exists to prevent.
  • The OPTIONS preflight is answered without a key — the browser sends it bare — and is cached for only ten minutes, so a change to the domain list shows up the same day.
  • The per-booking cap limits the damage of a leaked secret: with a cap, whoever steals it cannot create a half-million booking and ask for a refund. Zero means no cap, and the absolute ceiling is one million pesos in cents.
Base URL and headers
https://api.yareserva.com/api/v1/engine

X-YR-ENG-Key:       <keyId>
X-YR-ENG-Timestamp: <segundos Unix, UTC>
X-YR-ENG-Signature: sha256=<hex(HMAC-SHA256(secret, timestamp + "." + cuerpo))>
X-Idempotency-Key:  <solo al crear la reserva; opcional al cobrar>

How to sign

The mechanics are the same as the rest of the API: HMAC-SHA256 over “timestamp + "." + body”, with the secret as the key, in hexadecimal and prefixed with “sha256=”. There are two details of its own worth knowing before fighting a 401.

  • On a GET the body is empty, so you sign the empty string: what goes into the HMAC is the timestamp, the dot, and nothing else.
  • The URL does not go into the signature. Checking availability changes nothing, and including it would force both sides to normalize the query the same way, which is where two implementations always end up disagreeing.
  • The body is the exact bytes you send. Sign after serializing the JSON and do not reformat it.
  • Your clock can differ from ours by at most five minutes. Outside that the request is rejected even with a correct signature; sync with NTP.
  • A key that does not exist, a signature that does not match, a time out of range and a revoked connection all answer the same: 401 INVALID_SIGNATURE. Whoever is probing is not told which part failed.
  • The body cannot exceed 64 KB — eight rooms with their tokens fit with room to spare — and there is a cap of 300 requests per minute per IP.
bash — a POST
KEY=yreng_3f9a1c0b7d2e4a6f8b0c1d2e
SECRET=yreng_sk_...
BODY='{"offers":["eyJ2IjoxLCJyIjoi..."]}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -X POST https://api.yareserva.com/api/v1/engine/quote \
  -H "Content-Type: application/json" \
  -H "X-YR-ENG-Key: $KEY" \
  -H "X-YR-ENG-Timestamp: $TS" \
  -H "X-YR-ENG-Signature: sha256=$SIG" \
  --data-binary "$BODY"
bash — a GET, with an empty body
TS=$(date +%s)
# El punto y nada detrás: en un GET se firma la cadena vacía.
SIG=$(printf '%s.' "$TS" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl "https://api.yareserva.com/api/v1/engine/availability?checkIn=2026-11-20&checkOut=2026-11-22&adults=2" \
  -H "X-YR-ENG-Key: $KEY" \
  -H "X-YR-ENG-Timestamp: $TS" \
  -H "X-YR-ENG-Signature: sha256=$SIG"
Node.js
const crypto = require('crypto');

function firmar(secreto, payload) {
  // En un GET, payload es undefined y el cuerpo va vacío.
  const body = payload === undefined ? '' : JSON.stringify(payload);
  const ts = Math.floor(Date.now() / 1000).toString();
  const sig = crypto.createHmac('sha256', secreto).update(`${ts}.${body}`).digest('hex');
  return {
    body,
    headers: {
      'X-YR-ENG-Key': keyId,
      'X-YR-ENG-Timestamp': ts,
      'X-YR-ENG-Signature': `sha256=${sig}`,
    },
  };
}

// Manda `body` tal cual, sin volver a serializar el objeto.
Python
import hashlib, hmac, json, time

def firmar(secreto, payload=None):
    body = b"" if payload is None else json.dumps(payload, separators=(",", ":")).encode()
    ts = str(int(time.time()))
    sig = hmac.new(secreto.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return body, {
        "X-YR-ENG-Key": key_id,
        "X-YR-ENG-Timestamp": ts,
        "X-YR-ENG-Signature": "sha256=" + sig,
    }

The flow runs on signed offers

This is the part most often misread at the start, so it comes before the endpoints. There are no quote identifiers for you to store and hand back. What availability returns, for every rate of every room, is an offer: an opaque token signed by us. That token is the only thing you send back to quote and to book.

What you send to quote or to book
{ "offers": ["eyJ2IjoxLCJyIjoi...", "eyJ2IjoxLCJyIjoi..."] }

// Una sola habitación también se puede mandar como:
{ "offer": "eyJ2IjoxLCJyIjoi..." }

Rule: The price does NOT travel inside the token

The signature protects the room, the rate plan, the dates and the guests: nobody can edit those. The price is deliberately left out and is recomputed by our engine at quote time and again at booking time. If it travelled inside, anyone tampering with the token could buy a high-season night at last month’s price, and we would have two numbers for the same night. That is why your engine never sends an amount, and why it cannot trust a total it stored a while ago.

  • An offer is valid for thirty minutes from the moment it is issued. Plenty of time to fill in a form and little enough that inventory has not moved. After that you get 409 OFFER_EXPIRED and have to go back to availability.
  • Several offers in one array are several rooms of a single booking. Up to eight fit; more than that is a group and is quoted separately.
  • Every room in one booking has to share dates. Mixing them answers 422 MIXED_DATES.
  • Treat the token as opaque: do not parse it, do not shorten it, do not put it in a URL. It is only ever passed through as-is.
  1. The guest searches dates and your engine asks for availability. Each rate comes with its offer and with its price already computed.
  2. The guest picks, and your engine sends those offers to the quote. Back comes the total with taxes, how much is charged now, how much at the hotel, and the cancellation policy tiers.
  3. The guest fills in their details and accepts terms and privacy, and your engine creates the booking with the same offers and an idempotency key. The price is recomputed, checked against the key’s cap, and the inventory is sealed.
  4. Your engine asks for that booking’s payment intent and receives a client secret, the hotel’s account and the publishable key. The browser finishes the payment with Stripe.js.

Note: Here you book first and charge afterwards

The opposite of the Web Embed, which charges and then books. It is because the payment screen belongs to the hotel and we do not know when it will be shown: the booking is born with a balance due and the charge is opened when your engine asks for it.

Availability

GET/api/v1/engine/availability

What is free in a date range and at what price, with each rate’s signed offer.

Query parameters

NameTypeRequiredDescription
checkInAAAA-MM-DDYesArrival day.
checkOutAAAA-MM-DDYesDeparture day. If the range cannot be quoted you get 422 with field checkOut.
adultsintegerNoAdults. Two if you send nothing, and also if you send an impossible value: quoting for two is more useful than an error from a malformed URL.
childrenintegerNoChildren. Zero if you send nothing.
childAgesstringNoAges separated by commas: “8,12”. Some hotels price by age and some do not admit children under a certain age.
propertyIdUUIDNoOnly needed if the hotel has more than one property. Without it, its only property is used.

Request

http code
GET /api/v1/engine/availability?checkIn=2026-11-20&checkOut=2026-11-22&adults=2&children=1&childAges=8 HTTP/1.1
Host: api.yareserva.com
X-YR-ENG-Key: yreng_3f9a1c0b7d2e4a6f8b0c1d2e
X-YR-ENG-Timestamp: 1789459391
X-YR-ENG-Signature: sha256=9c1f0a7d4b2e6c8a1f3d5b7e9c0a2d4f6b8e0c2a4d6f8b0c2e4a6d8f0b2c4e6a

Response

200 · Trimmed to one room and one rate
{
  "data": {
    "checkIn": "2026-11-20",
    "checkOut": "2026-11-22",
    "nights": 2,
    "adults": 2,
    "children": 1,
    "currency": "MXN",
    "pricesIncludeTaxes": true,
    "rooms": [
      {
        "roomTypeId": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
        "name": "Habitación Doble",
        "description": "Cama king, vista al jardín.",
        "images": ["https://cdn.yareserva.com/…/doble-1.jpg"],
        "amenities": ["wifi", "air_conditioning"],
        "beds": "1 king",
        "maxGuests": 3,
        "perPerson": false,
        "rates": [
          {
            "offer": "eyJ2IjoxLCJyIjoi...",
            "ratePlanId": "2b3c4d5e-6f70-4819-a2b3-c4d5e6f70819",
            "ratePlan": "Tarifa Flexible",
            "description": "Cancela gratis hasta 48 h antes.",
            "refundable": true,
            "cancelText": "Cancelación gratuita hasta el 18 de noviembre.",
            "freeCancelUntil": "2026-11-18T18:00:00Z",
            "paymentTiming": "pay_now",
            "payNowCents": 440300,
            "payAtHotelCents": 0,
            "included": [{ "name": "Wifi", "type": "wifi" }],
            "totalCents": 440300,
            "nightlyFromCents": 185000,
            "taxesCents": 70300,
            "currency": "MXN",
            "nights": 2,
            "unitsLeft": 4
          }
        ]
      }
    ]
  }
}

Nothing being free is not an error: you get 200 with an empty “rooms”. Show “no availability” and offer other dates.

  • “pricesIncludeTaxes” decides whether your screen says “taxes included” or “plus taxes”. It is the hotel’s decision, not yours.
  • “payNowCents” and “payAtHotelCents” always add up to “totalCents”. Some plans are charged in full at booking and others are paid on arrival.
  • “unitsLeft” is how many rooms of that type are left. It is for the “only two left” line on your screen, not for booking more than exist.
  • “freeCancelUntil” comes back empty when the rate has no free window.

Quote

POST/api/v1/engine/quote

Recomputes what the guest chose: total with taxes, how much now and how much at the hotel, and the cancellation policy tiers for each room.

Request body

NameTypeRequiredDescription
offersstring[]YesThe chosen offers, one per room, up to eight. For a single one you can also use the “offer” field with the bare token.
offerstringNoShortcut for a single room. If you send “offers”, this one is ignored.
propertyIdUUIDNoSame as in availability: only if the hotel has more than one.

Request

json code
{
  "offers": ["eyJ2IjoxLCJyIjoi...", "eyJ2IjoxLCJyIjoi..."]
}

Response

200 · The quote
{
  "data": {
    "propertyId": "0f1e2d3c-4b5a-4968-8776-655443332211",
    "checkIn": "2026-11-20",
    "checkOut": "2026-11-22",
    "nights": 2,
    "adults": 2,
    "children": 1,
    "currency": "MXN",
    "totalCents": 880600,
    "taxesCents": 140600,
    "payNowCents": 880600,
    "payAtHotelCents": 0,
    "requiresCard": false,
    "rooms": [
      {
        "offer": "eyJ2IjoxLCJyIjoi...",
        "roomTypeId": "1a2b3c4d-5e6f-4708-9a1b-2c3d4e5f6071",
        "roomName": "Habitación Doble",
        "ratePlanId": "2b3c4d5e-6f70-4819-a2b3-c4d5e6f70819",
        "ratePlan": "Tarifa Flexible",
        "totalCents": 440300,
        "taxesCents": 70300,
        "nightlyFromCents": 185000,
        "payNowCents": 440300,
        "payAtHotelCents": 0,
        "currency": "MXN",
        "nights": 2,
        "paymentTiming": "pay_now",
        "requiresGuarantee": false,
        "cancellationRules": [
          { "hoursBefore": 48, "penaltyType": "none", "penaltyValue": 0, "appliesTo": "total" },
          { "hoursBefore": 0, "penaltyType": "nights", "penaltyValue": 1, "appliesTo": "total" }
        ]
      }
    ]
  }
}

Careful: Show the total that arrives, not the one you add up

Lodging taxes vary by state and do not apply the same way on every stay. Recomputing on the engine side produces differences the guest discovers on arrival. The total is “totalCents” and what is charged now is “payNowCents”.

The cancellation tiers come raw rather than as a ready-made sentence, because your engine draws its own screen: with the sentence you could not show “free until Thursday, one night after that” in your language or your format. “requiresCard” warns that nothing is charged now but some policy asks for a card as a guarantee.

Create the booking

POST/api/v1/engine/bookings

Seals the inventory and creates the booking with a balance due. Requires X-Idempotency-Key.

Request body

NameTypeRequiredDescription
offersstring[]YesThe same offers you quoted. “offer” also works for a single one.
guest.firstNamestringYesFirst name. Empty answers 422 with field guest.firstName.
guest.lastNamestringYesLast name.
guest.emailstringYesThis is where the booking confirmation is sent.
guest.phonestringNoIn international format, with a plus sign and country code.
guest.countrystringNoThe holder’s nationality. Used in the hotel’s reporting.
guest.birthdateAAAA-MM-DDNoThe holder’s date of birth, when the hotel asks for it.
acceptedTermsbooleanYesThat the guest accepted the terms. Stored in the booking’s history alongside the IP: that is where it holds up if anyone disputes it.
acceptedPrivacybooleanYesThat they accepted the privacy notice. Stored the same way.
guestIpstringNoThe guest’s IP when your engine runs on the hotel’s server: there the one we see is yours, not the booker’s. Without this field we use the one we see.
userAgentstringNoThe same for the guest’s browser.
propertyIdUUIDNoOnly if the hotel has more than one property.

Request

With the X-Idempotency-Key header
POST /api/v1/engine/bookings
X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f

{
  "offers": ["eyJ2IjoxLCJyIjoi..."],
  "guest": {
    "firstName": "Ana",
    "lastName": "Ruiz",
    "email": "ana@correo.mx",
    "phone": "+525512345678",
    "country": "MX",
    "birthdate": "1990-04-12"
  },
  "acceptedTerms": true,
  "acceptedPrivacy": true,
  "guestIp": "189.203.11.42",
  "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_2 like Mac OS X)"
}

Response

201 · Booking created, with a balance due
{
  "data": {
    "id": "7b0c5d2e-1f3a-4c6b-9d8e-2a1b3c4d5e6f",
    "code": "YR-8K3M2Q",
    "status": "pending",
    "checkIn": "2026-11-20",
    "checkOut": "2026-11-22",
    "adults": 2,
    "children": 1,
    "rooms": 1,
    "totalCents": 440300,
    "paidCents": 0,
    "dueCents": 440300,
    "currency": "MXN",
    "guest": {
      "firstName": "Ana",
      "lastName": "Ruiz",
      "email": "ana@correo.mx",
      "phone": "+525512345678"
    },
    "cancelledAt": null,
    "penaltyCents": 0,
    "retry": false
  }
}

Rule: The idempotency key is required and is checked inside a lock

Without the header the answer is 400 IDEMPOTENCY_KEY_REQUIRED: we do not invent one for you, because then a retry after a dropped connection would create a second booking, and that is only discovered when the hotel sees two rooms blocked. The key check and the sale happen inside a lock keyed on the connection and the key, so two simultaneous POSTs with the same key cannot both pass the empty check: they are serialized and only one sells.

  • The key accepts letters, digits and . _ : - and fits in 100 characters. A UUID v4 will do. A key with other characters answers 422 with field idempotencyKey.
  • A retry with the same key returns 200 — not 201 — with the same booking and “retry”: true. A 201 means this request sold; a 200 means it is only showing you what already existed.
  • The key is recorded alongside the booking it created. Reusing it with different offers does not create anything different: it returns the first booking. If the data changes, change the key.
  • The price is recomputed before selling and checked against the connection’s cap. If it exceeds it you get 422 BOOKING_OVER_LIMIT and nothing is created.

Look up a booking

GET/api/v1/engine/bookings/:code

The status and balance of a booking by its code. This is what your thank-you page and your “my booking” screen render.

Path parameters

NameTypeRequiredDescription
codestringYesThe code returned on creation, such as YR-8K3M2Q.

Response

200 · Already paid
{
  "data": {
    "id": "7b0c5d2e-1f3a-4c6b-9d8e-2a1b3c4d5e6f",
    "code": "YR-8K3M2Q",
    "status": "confirmed",
    "checkIn": "2026-11-20",
    "checkOut": "2026-11-22",
    "adults": 2,
    "children": 1,
    "rooms": 1,
    "totalCents": 440300,
    "paidCents": 440300,
    "dueCents": 0,
    "currency": "MXN",
    "guest": {
      "firstName": "Ana",
      "lastName": "Ruiz",
      "email": "ana@correo.mx",
      "phone": "+525512345678"
    },
    "cancelledAt": null,
    "penaltyCents": 0,
    "retry": false
  }
}

Rule: A connection only sees what it created

Looking up, cancelling or charging a booking that came in through another connection — or the front desk, or an OTA — answers 404, exactly like a code that does not exist. They are deliberately indistinguishable: if they were not, whoever walked through codes would know which ones they hit, and codes are short.

The booking does not come in full: cost per night, internal notes and commission are left out. Those are the hotel’s data, not the sale’s. “dueCents” is what is left to charge and is never negative.

Cancel

POST/api/v1/engine/bookings/:code/cancel

Cancels applying the policy frozen when the booking was confirmed, and says how much is withheld and how much is left to refund.

Request body

NameTypeRequiredDescription
reasonstringNoWhy it is being cancelled. Visible in the hotel’s panel. Empty is accepted — cancelling without saying why is normal on a hotel website — and “Cancelled by the guest from the hotel’s engine” is stored. Over 300 characters is trimmed.

Request

json code
{ "reason": "Cambio de planes del huésped" }

Response

200 · Cancelled within the free window
{
  "data": {
    "booking": {
      "id": "7b0c5d2e-1f3a-4c6b-9d8e-2a1b3c4d5e6f",
      "code": "YR-8K3M2Q",
      "status": "cancelled",
      "totalCents": 440300,
      "paidCents": 440300,
      "dueCents": 0,
      "currency": "MXN",
      "cancelledAt": "2026-11-16T09:12:00Z",
      "penaltyCents": 0,
      "retry": false
    },
    "penaltyCents": 0,
    "refundDueCents": 440300,
    "currency": "MXN",
    "refundedByHotel": true,
    "refundedByUsNote": "El cargo nació en la cuenta Stripe del hotel: la devolución la emite el hotel."
  }
}

Rule: This does NOT refund: it says how much

The money is in the hotel’s Stripe account, because that is where the charge was born. The hotel executes the refund. What this endpoint returns is “refundDueCents”, how much is left to give back after the penalty. Do not promise the guest the money has already gone out: tell them what they are owed and that the hotel issues it.

The policy is not recomputed against today’s. The guest bought under certain terms, and applying different ones afterwards — even if the hotelier edited the rate plan in the meantime — would charge them a penalty they never accepted.

Charge

POST/api/v1/engine/bookings/:code/payment-intent

Opens the charge for a booking with a balance due and returns what your page needs to mount the card form.

Request body

NameTypeRequiredDescription
amountCentsintegerNoHow much to charge. At zero, or absent, the entire outstanding balance is charged. It can never exceed it: more answers 422 with field amountCents.

Request

X-Idempotency-Key is optional here
POST /api/v1/engine/bookings/YR-8K3M2Q/payment-intent
X-Idempotency-Key: 8c2e1f04-5a6b-4c7d-8e9f-0a1b2c3d4e5f

{ "amountCents": 440300 }

Response

201 · Ready to charge in the browser
{
  "data": {
    "paymentIntentId": "pi_3Qk…",
    "clientSecret": "pi_3Qk…_secret_9c1f…",
    "stripeAccountId": "acct_1QkA2B3C4D5E6F7G",
    "publishableKey": "pk_live_51H…",
    "amountCents": 440300,
    "currency": "MXN"
  }
}
  • The charge is direct on the hotel’s Connect account, like any of its own direct sales: the merchant of record is the hotel. Our share travels as a platform fee.
  • Without “stripeAccountId” the client secret does not resolve, and the error the guest sees is “No such payment_intent”, which from outside looks like the booking does not exist. Initialize Stripe.js with the account and the publishable key from the response.
  • The publishable key is given by the server and not configured by the hotelier: it is our Connect platform key and it differs between test and production.
  • If you send X-Idempotency-Key, two clicks on “pay” give the same intent instead of two charges you would have to refund. If you do not, a stable one per booking and amount is generated, which does the same.
In the browser, with Stripe.js
// Tu servidor pide la intención (firmada) y le pasa esto a la página.
const { clientSecret, publishableKey, stripeAccountId } = await pedirIntencionASuBackend(code);

// La cuenta del hotel: el cargo nace y se queda ahí.
const stripe = Stripe(publishableKey, { stripeAccount: stripeAccountId });
const elements = stripe.elements({ clientSecret });
elements.create('payment').mount('#pago');

const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url: 'https://www.mihotel.mx/reserva/gracias?code=' + code },
});

// No marques la reserva como pagada desde aquí: vuelve a consultarla.

Careful: The status is changed by the payment, not by your page

When the payment truly lands, the booking moves to confirmed on its own. Your thank-you page looks the booking up again by its code and shows whatever comes back. If the guest closes the tab mid-authentication, the booking keeps its balance due and the room is released when its payment deadline expires.

Engine errors

The HTTP status says what kind of problem it is and “error.code” says which one. On VALIDATION_ERROR, “details.field” says exactly which field to check.

Errors

HTTPCodeWhenWhat to do
400IDEMPOTENCY_KEY_REQUIREDA POST to /bookings without the X-Idempotency-Key header.Send the key. It is not invented for you.
401INVALID_SIGNATUREUnknown key, signature that does not match, time outside the five-minute window, or revoked connection.Check the clock first. Then check that you sign the exact bytes you send, and that on GETs you sign the empty string.
403ORIGIN_NOT_ALLOWEDThe request arrived with an Origin and that domain is not on the key’s list, or the key has no declared domains.Do not retry. Either call from the server, or have the hotel declare the domain in its panel.
404NOT_FOUNDThere is no booking with that code, or there is but this connection did not create it.Check the code. Both cases answer the same on purpose.
409OFFER_EXPIREDThe offer is over thirty minutes old, or that rate is no longer available.Ask for availability again and show what is left.
409PRICE_CHANGEDThe price changed since it was quoted.Quote again and show the new price before charging.
409BOOKING_ALREADY_CANCELLEDThat booking is already cancelled.Read it and show where it stands. Cancelling again changes nothing.
409NOTHING_TO_CHARGEThat booking has no outstanding balance.Send the guest to the thank-you page.
409NO_PROPERTYThe hotel has not registered any property yet.Do not retry. It is fixed in the hotel’s panel.
422VALIDATION_ERRORA value that does not work. “details.field” says which: checkIn, checkOut, propertyId, offers, body, guest.firstName, guest.lastName, guest.email, idempotencyKey or amountCents.Fix that field and send again.
422BOOKING_OVER_LIMITThe booking exceeds the per-booking cap the hotel set on this key.Nothing was created. The hotel raises the cap from its panel if appropriate.
422TOO_MANY_ROOMSMore than eight rooms in a single booking.A group is quoted separately, with the hotel.
422MIXED_DATESThe rooms in the cart do not share dates.Split them into two bookings.
422CHILDREN_NOT_ADMITTEDThe hotel is adults only.Say so before asking for the guest’s details.
422CHILD_BELOW_MIN_AGEThe hotel does not admit children under that age.Show the minimum age in your search form.
413PAYLOAD_TOO_LARGEThe body is over 64 KB.Send fewer offers: eight fit with room to spare.
429RATE_LIMIT_EXCEEDEDMore than 300 requests per minute from the same IP.Wait for what Retry-After says and retry.
503PAYMENTS_NOT_CONFIGUREDOnline payment is not configured for this hotel.Availability, quoting and booking keep working. The only thing you cannot do is charge; tell the hotel.

Integrating a hotel?

Credentials come out of the hotel’s panel, and sandbox ones are created the same day. If the hotel does not have an account yet, start by registering it.