Rooms and guests
How to describe rooms and guests in Quote and Prebook, which guest details Confirm asks for, and the validation rules that reject passenger data before a booking can close.
Rooms are distributions
Each entry in distributions[] is one room. Inside it, persons[] lists everyone who sleeps in that room. Rooms in one booking can have different occupancy.
| Step | Array | Each person carries |
|---|---|---|
| Quote, Quote single | distributions[].persons[] | age (integer, required) |
| Prebook | distributions[].persons[] (person also works: see persons or person) | requestedAge (required) plus the fields Confirm asks for |
| Book, booking detail | distributions[].person[] | The same data, now with ids such as TST-1464-0-1 (booking, room, person) |
- With two distributions, every combination in the Quote response has two entries in
rooms[]. The platform pre-computes the room combinations for you. - Prebook must repeat the Quote's rooms in the same order, with the same number of people in each room. Reordering rooms in your UI after the Quote produces an "order changed" error.
Occupancy limits
| Limit | Value | |
|---|---|---|
| Rooms per booking | 1 to 4. 0 or 5 is rejected with 400. | |
| People per room | 1 to 6, children included. A seventh person is rejected with 400. | |
| Children per room | At most 5 | |
| People per booking | 15, of whom at most 14 children | |
| Child ages | 0-17 inclusive | |
| Adults per room | At least 1 |
Enforce these limits in your search form, before you call Quote. The room and people counts are rejected as soon as the request arrives. The child and adult rules are not: a room with no adult gets past the first check, so your form is the only thing that stops it early.
Ages: Quote and Prebook
Quote takes age. Prebook takes requestedAge. Both are the guest's age at the end of the stay, on the check-out date. A child who turns 6 during the stay is 6.
requestedAgeis mandatory for every guest in Prebook, even whenrequiredFielddoes not list it.- A child's
requestedAgemust equal theageyou quoted, or Prebook fails with "Child age different from availability". This is why the Quote must also use the age at check-out, not the age today. - If you send
birthDate, it must agree withrequestedAge.
/** Age on the check-out date. Send it as `age` in Quote and `requestedAge` in Prebook. */
export function ageAtCheckout(birthDate: string, checkOut: string): number {
const [by, bm, bd] = birthDate.split('-').map(Number); // yyyy-MM-dd
const [cy, cm, cd] = checkOut.split('-').map(Number);
const birthdayStillAhead = cm < bm || (cm === bm && cd < bd);
return cy - by - (birthdayStillAhead ? 1 : 0);
}
ageAtCheckout('2019-11-12', '2026-11-14'); // 7: the child turns 7 during the stayContact person and room holders
Confirm returns requiredField with three lists of RequiredPassengerData values. Each list applies to a different position in distributions[].
| List | Applies to | Example from a Confirm response |
|---|---|---|
contactPerson | The first person of the first room | FIRST_NAME, LAST_NAME, TITLE, PHONE, EMAIL |
roomHolders | The first person of every other room | FIRST_NAME, EMAIL |
otherPersons | Everyone else | FIRST_NAME, LAST_NAME, TITLE |
- Put the adult who has an email address and a phone number first in room 1. That person becomes the contact person.
- Build the guest form from each Confirm response, not from a fixed template: the lists tell you exactly which data to collect.
- Prebook enforces the lists. A missing field gets
400with one message per person and field, for exampleMissing the following required field of the contact person: Birthdate. - Suppliers differ. A test supplier asked for
BIRTH_DATEfor every guest, including the contact person. - The API receives the names you send for every room, but only mandatory data is forwarded to suppliers.
Map requiredField to person fields
Each requiredField value tells you which PersonVO field to fill in Prebook.
requiredField value | Send in the person | Rule | |
|---|---|---|---|
TITLE | courtesyTitle | MISTER, MRS or MS | |
FIRST_NAME | name | First name, Latin script | |
LAST_NAME | lastName | Surname, Latin script | |
BIRTH_DATE | birthDate | yyyy-MM-dd, consistent with requestedAge at the end of the trip | |
DOCUMENT | documentNumber and documentType | A type without a number is rejected | |
PASSPORT | documentNumber and documentType: "PASSPORT" | The type has to be PASSPORT when the required type is PASSPORT | |
DOCUMENT_EXPIRY_DATE | passportExpirationDate | Must be after the trip ends | |
EMAIL | email | Email format | |
PHONE | phoneCountryCode and phone | For example +34 and 600000000 | |
COUNTRY | countryId | ||
ADDRESS | address with street, city, postCode | ||
SOCIAL_INSURANCE_NUMBER | socialInsuranceNumber | ||
BILLING_DOCUMENT | billingNumber | ||
ACADEMY_TITLE | academyTitle | Dr, Prof or ProfDr. Mapping inferred from the field name. | |
EMERGENCY_CONTACT, DOCUMENT_DATE_OF_ISSUE, FREQUENT_TRAVELLER_NUMBER, INVOICE_DATA, SEAT_SELECTION | No hotel mapping documented | If one appears for a hotel, ask your Nava account manager. |
When DOCUMENT (not PASSPORT) is required, documentType is probably IDENTITY_CARD or NIE for a national document. This is not documented, so test it in your test account.
Validation rules
Field patterns
The spec validates these PersonVO fields. A value that fails is rejected at Prebook.
| Field | Rule | Passes | Fails |
|---|---|---|---|
name, lastName | ^[\p{IsLatin} .'-]+$: Latin letters, space, full stop, apostrophe, hyphen | Omar, O'Neil, Gil-Haddad | Any name in Arabic, Cyrillic, Greek or CJK script |
phone | [0-9]+, max 15 characters | 600000000 | 600 000 000, +34600000000 |
phoneCountryCode | \+[0-9]*$, 2-4 characters | +34, +966 | 0034, 00966, 34 |
documentNumber | ^[A-Za-z0-9]+(?:-[A-Za-z0-9]+){0,2}$, max 20 characters | X1234567, AB-123-45 | AB 1234, A-B-C-D |
email | Email format | ana@example.com | ana@ |
The platform checks phone numbers with Google's libphonenumber (com.google.i18n.phonenumbers). Validate them in your checkout with a port of the same library.
Business rules
The API also checks the guests against each other and against the Quote.
- Two guests with identical names in one booking are rejected. Add a suffix such as
JRto tell a father and son apart. - A
documentTypewithout adocumentNumberis rejected. - Two guests with the same document number are rejected.
- A passport that expires before the trip ends is rejected.
birthDatemust be consistent withrequestedAge.- A child's
requestedAgemust equal the quoted age, and the rooms must keep the Quote's order and person counts.
Validation errors the API returns
These are the passenger errors the API documents. Check each one before Prebook so guests fix their input in your form.
| Error | Check before Prebook |
|---|---|
Missing the following required field of the contact person: Birthdate, one message per person and field | Every field Confirm's requiredField lists for that position is filled in |
| Phone incorrect | phoneCountryCode and phone match the patterns and form a valid number |
| Invalid email format | email |
| Duplicate passenger names | name plus lastName is unique in the booking |
| Birthdate does not match requested age | The age on the check-out date, from birthDate, equals requestedAge |
| Child age different from availability / order changed | requestedAge equals the quoted age, and the room order is unchanged |
| Duplicate document | documentNumber is unique in the booking |
| Passport expires before end of trip | passportExpirationDate is after the check-out date |
| Document type informed without document number | documentType is only sent with a documentNumber |
| Different number of persons in distribution / order changed | Same rooms, same order and same person counts as the Quote |
The list does not include the field patterns above. Check those too.
// Mirrors the spec patterns for PersonVO. Java's \p{IsLatin} is \p{Script=Latin} in JavaScript (u flag).
const LATIN_NAME = /^[\p{Script=Latin} .'-]+$/u;
const PHONE = /^[0-9]+$/; // max 15
const PHONE_COUNTRY_CODE = /^\+[0-9]*$/; // 2 to 4 characters
const DOCUMENT_NUMBER = /^[A-Za-z0-9]+(?:-[A-Za-z0-9]+){0,2}$/; // max 20
export type GuestInput = {
name?: string;
lastName?: string;
phoneCountryCode?: string;
phone?: string;
documentType?: 'IDENTITY_CARD' | 'NIE' | 'PASSPORT';
documentNumber?: string;
};
export function validateGuest(p: GuestInput): string[] {
const errors: string[] = [];
for (const field of ['name', 'lastName'] as const) {
const v = p[field];
if (v !== undefined && !LATIN_NAME.test(v)) {
errors.push(`${field}: Latin letters only. Transliterate other scripts.`);
}
}
if (p.phone !== undefined && (!PHONE.test(p.phone) || p.phone.length > 15)) {
errors.push('phone: digits only, at most 15.');
}
const cc = p.phoneCountryCode;
if (cc !== undefined && (!PHONE_COUNTRY_CODE.test(cc) || cc.length < 2 || cc.length > 4)) {
errors.push('phoneCountryCode: "+" followed by digits, for example +34.');
}
if (p.documentType && !p.documentNumber) {
errors.push('documentNumber: required when documentType is set.');
}
const doc = p.documentNumber;
if (doc !== undefined && (!DOCUMENT_NUMBER.test(doc) || doc.length > 20)) {
errors.push('documentNumber: letters and digits, at most two hyphens, at most 20 characters.');
}
return errors;
}
/** Rules across the whole booking: one inner array per room, in Quote order. */
export function validateBooking(rooms: GuestInput[][]): string[] {
const errors: string[] = [];
const names = new Set<string>();
const documents = new Set<string>();
rooms.flat().forEach((p, i) => {
const fullName = `${p.name ?? ''} ${p.lastName ?? ''}`.trim().toUpperCase(); // stricter than needed, on purpose
if (fullName && names.has(fullName)) errors.push(`Guest ${i + 1}: duplicate name. Add a suffix such as JR.`);
names.add(fullName);
if (p.documentNumber) {
const d = p.documentNumber.toUpperCase();
if (documents.has(d)) errors.push(`Guest ${i + 1}: duplicate document number.`);
documents.add(d);
}
});
return errors;
}persons or person
The docs send the Prebook array as distributions[].persons, while the original OpenAPI definition names it person. Both work.
- Send
persons. It is the recommended name. personis accepted too: Prebook succeeds with either name.- Responses name the array
person: the Prebook echo, Book and booking detail. Some documentation samples usepersonsandbirthdate. When you read responses, accept bothpersonandpersons, and bothbirthDateandbirthdate.
const peopleIn = (room: any) => room.person ?? room.persons ?? [];
const birthDateOf = (p: any) => p.birthDate ?? p.birthdate;Special requests
accommodation.commentToAccommodation in Prebook is the only way to pass a request to the hotel. It is free text forwarded to the hotel. Structured special requests are not supported, and bedding type cannot be sent.
- Keep it short and plain, for example
Late arrival around 23:00orAdjoining rooms if possible. - Booked services read from
getBookingsspell the fieldcommentsToAccommodation(plural). Map both names.
Worked example: two rooms with a child
A family books two rooms from 2026-11-10 to 2026-11-14. Room 1: Ana Ruiz (35) and Luis Ruiz (33). Room 2: Omar Haddad (40) and his son Leo Haddad, who turns 7 on 12 November, during the stay. Leo is 7 in both Quote and Prebook. Certification asks for a two-room booking and a booking with children, so this makes a good test-account booking.
1. Quote with two distributions
{
"checkIn": "2026-11-10",
"checkOut": "2026-11-14",
"distributions": [
{ "persons": [{ "age": 35 }, { "age": 33 }] },
{ "persons": [{ "age": 40 }, { "age": 7 }] }
],
"language": "EN",
"sourceMarket": "AE",
"tripType": "ONLY_HOTEL",
"timeout": 8000,
"filter": { "bestCombinations": true, "maxCombinations": 4 },
"destinationId": "MAD"
}Every combination in the response has two rooms in rooms[].
2. Confirm tells you what to collect
{
"auditData": {
"authToken": "[REDACTED]",
"traceId": "D4AE54C6-…"
},
"requiredField": {
"contactPerson": ["FIRST_NAME", "LAST_NAME", "TITLE", "PHONE", "EMAIL"],
"otherPersons": ["FIRST_NAME", "LAST_NAME", "TITLE"],
"roomHolders": ["FIRST_NAME", "EMAIL"]
}
}3. Prebook with the same rooms in the same order
{
"accommodation": {
"combinationKey": "<key from Confirm>",
"commentToAccommodation": "Adjoining rooms if possible"
},
"distributions": [
{
"persons": [
{
"name": "Ana",
"lastName": "Ruiz",
"requestedAge": 35,
"courtesyTitle": "MRS",
"email": "ana@example.com",
"phoneCountryCode": "+34",
"phone": "600000000"
},
{ "name": "Luis", "lastName": "Ruiz", "requestedAge": 33, "courtesyTitle": "MISTER" }
]
},
{
"persons": [
{
"name": "Omar",
"lastName": "Haddad",
"requestedAge": 40,
"courtesyTitle": "MISTER",
"email": "omar@example.com"
},
{ "name": "Leo", "lastName": "Haddad", "requestedAge": 7, "courtesyTitle": "MISTER" }
]
}
]
}- Ana is the first person of room 1, so she is the contact person and carries the
contactPersonfields: names, title, phone and email. - Omar is the first person of room 2, so he is its room holder and carries the
roomHoldersfields (FIRST_NAME,EMAIL). - Luis and Leo follow
otherPersons(FIRST_NAME,LAST_NAME,TITLE). That is why the child also has acourtesyTitle. - Leo's
requestedAge(7) equals his quotedage(7). - The array is
persons, the recommended name.personworks too (see persons or person).
Related
The booking flow
Five calls, one of them optional, take a guest from search results to a confirmed room. Every call returns a new combinationKey, and the next call must send that newest key.
Booking statuses
What each status means at service and booking level, what the guest should see, how to handle on-request bookings, and which status changes after booking are normal.