Certification
What the review before go-live covers, the three test-account bookings it asks for, what to prepare, how to make the bookings an automated test, and a go-live checklist.
What the review is
Before you get production credentials, the request and response of every call you use are reviewed, together with three bookings you make in your test account. Expect the review to take a few days.
Each has its own base URL and its own credentials. See Environments and credentials.
The three required bookings
Make each one in your test account as a complete flow: Quote, Confirm, Prebook and Book, on one token, with the newest combinationKey at every step. Use real bookings, not fakeBooking: a fake booking is not saved.
1. One room, two adults
{
"distributions": [
{ "persons": [{ "age": 30 }, { "age": 30 }] }
]
}This is the standard booking.
2. Two rooms
{
"distributions": [
{ "persons": [{ "age": 30 }, { "age": 30 }] },
{ "persons": [{ "age": 45 }, { "age": 42 }] }
]
}Every combination's rooms[] lists two rooms. Prebook repeats both rooms in the same order, and the first person of room 2 is its room holder.
3. Adults and children
{
"distributions": [
{ "persons": [{ "age": 30 }, { "age": 30 }] },
{ "persons": [{ "age": 40 }, { "age": 5 }] }
]
}The child's quoted age is their age at checkout, and their requestedAge at Prebook must be the same value.
{
"accommodation": {
"combinationKey": "<key from Confirm>",
"commentToAccommodation": "Adjoining rooms if possible"
},
"distributions": [
{
"persons": [
{ "name": "Ana", "lastName": "Ruiz", "requestedAge": 30, "courtesyTitle": "MRS", "email": "ana@example.com", "phoneCountryCode": "+34", "phone": "600000000" },
{ "name": "Luis", "lastName": "Ruiz", "requestedAge": 30, "courtesyTitle": "MISTER" }
]
},
{
"persons": [
{ "name": "Marta", "lastName": "Gil", "requestedAge": 40, "courtesyTitle": "MS", "email": "marta@example.com" },
{ "name": "Leo", "lastName": "Gil", "requestedAge": 5, "courtesyTitle": "MISTER" }
]
}
]
}Ana is the contact person and Marta the room holder of room 2. Send the fields Confirm's requiredField asks for in your flow, not the ones in this sample. See Rooms and guests.
What to prepare
Have these ready when you ask for the review.
- Logs with the
traceIdandx-request-idof every call, withauditData.authTokenredacted. For calls without a trace id, keep the full request and response. - Proof that you handle warnings (
PRICE_CHANGE,CANCELLATION_POLICIES_CHANGE),RQand errors. - A staging site, if you can.
- Evidence that you use one token per flow and send the newest
combinationKeyat every step.
Automate the three bookings
Build the three bookings into an automated test suite that runs against your test account from day one. The evidence then regenerates on every run, and a regression in your flow fails a build before it reaches a reviewer or a guest.
The sketch below uses Vitest. It reads credentials from environment variables and builds the Quote ages and the Prebook guests from the same fixtures so they can't drift apart. It refuses to run unless NAVA_ENV is test, a flag you set only next to your test credentials, and NAVA_BASE_URL is the sandbox. The three certification cases are real bookings in your test account. Only the failure-path test uses fakeBooking.
Test suppliers may not behave exactly like real suppliers. The suite expects each certification booking to come back confirmed and prints the actual status when it doesn't. If that happens, ask your Nava account manager how the test suppliers respond.
// The three certification bookings, run in your test account.
// Env: NAVA_ENV=test, plus your TEST credentials in NAVA_BASE_URL, NAVA_USERNAME,
// NAVA_PASSWORD and NAVA_MICROSITE_ID.
import { describe, expect, it } from 'vitest';
// Real bookings: run only against the sandbox, with test credentials and NAVA_ENV=test.
// Set NAVA_ENV=test only where the test credentials live.
if (process.env.NAVA_ENV !== 'test' || !process.env.NAVA_BASE_URL?.startsWith('https://sandbox.nava.travel/')) {
throw new Error('These tests make real bookings: run them only against the sandbox, with test credentials and NAVA_ENV=test');
}
const BASE = process.env.NAVA_BASE_URL!;
// Future dates only: a search for past dates returns nothing.
const day = (n: number) => new Date(Date.now() + n * 86_400_000).toISOString().slice(0, 10);
const [checkIn, checkOut] = [day(42), day(46)];
type Person = { requestedAge: number } & Record<string, string | number>;
type Room = { persons: Person[] };
async function call(token: string | null, method: string, path: string, body?: unknown, timeoutMs = 120_000) {
const res = await fetch(BASE + path, {
method,
headers: {
...(token ? { 'auth-token': token } : {}),
'Accept-Encoding': 'gzip',
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(timeoutMs),
});
const json = await res.json().catch(() => ({}));
if (json?.auditData?.authToken) json.auditData.authToken = '[REDACTED]';
console.log(method, path, res.status, json?.auditData?.traceId); // evidence for the review
if (!res.ok) throw new Error(`${method} ${path} failed with HTTP ${res.status}`);
return json;
}
// Quote ages come from the Prebook guests, so the two always match.
const quoteRooms = (rooms: Room[]) =>
rooms.map((r) => ({ persons: r.persons.map((p) => ({ age: p.requestedAge })) }));
/** One flow on one token: Quote, Confirm, Prebook, Book. */
async function bookFlow(rooms: Room[], orderId: string, fakeBooking?: { status: 'BOOK_ERROR' }) {
const { token } = await call(null, 'POST', '/authentication/authenticate', {
username: process.env.NAVA_USERNAME,
password: process.env.NAVA_PASSWORD,
micrositeId: process.env.NAVA_MICROSITE_ID,
});
const quote = await call(token, 'POST', '/booking/accommodations/quote', {
checkIn, checkOut, distributions: quoteRooms(rooms), destinationId: 'MAD',
language: 'EN', sourceMarket: 'AE', tripType: 'ONLY_HOTEL', timeout: 8000,
filter: { bestCombinations: true, maxCombinations: 4, includeOnRequestOptions: false },
}, 60_000);
const hotel = quote.accommodations?.find((a: any) => a.combinations?.length);
if (!hotel) throw new Error('No availability in the test account for these dates');
const path = `/booking/accommodations/${hotel.code}`;
let key = hotel.combinations[0].combinationKey; // key 1
const confirm = await call(token, 'POST', `${path}/confirm`, { accommodation: { combinationKey: key } });
if (confirm.warnings?.length) console.log('warnings', confirm.warnings); // your UI asks the guest here
key = confirm.accommodation.combination.combinationKey; // key 2
const prebook = await call(token, 'POST', `${path}/prebook`, {
accommodation: { combinationKey: key },
distributions: rooms, // same rooms, same order
});
key = prebook.accommodation.combination.combinationKey; // key 3
// Book exactly once. A retry, in code or in the test runner, can book twice.
const booked = await call(token, 'POST', `${path}/book`, {
accommodation: { combinationKey: key },
externalReference: orderId,
...(fakeBooking ? { fakeBooking } : {}),
}, 180_000);
return { token, booked };
}
// Use your application's real status handler here, not a copy.
function outcome(status: string) {
switch (status) {
case 'BOOKED': return 'confirmed';
case 'RQ': case 'PENDING_BOOK': return 'pending';
case 'PRICE_ERROR': return 'review';
case 'BOOK_ERROR': case 'NOT_BOOKED': return 'failed';
default: return 'review'; // an unknown status is never success
}
}
// Latin-script names. Match the fields to what requiredField asks for in your test account.
const ana = { name: 'Ana', lastName: 'Ruiz', requestedAge: 30, courtesyTitle: 'MRS',
email: 'ana@example.com', phoneCountryCode: '+34', phone: '600000000' };
const luis = { name: 'Luis', lastName: 'Ruiz', requestedAge: 30, courtesyTitle: 'MISTER' };
const omar = { name: 'Omar', lastName: 'Haddad', requestedAge: 45, courtesyTitle: 'MISTER', email: 'omar@example.com' };
const sara = { name: 'Sara', lastName: 'Haddad', requestedAge: 42, courtesyTitle: 'MRS' };
const marta = { name: 'Marta', lastName: 'Gil', requestedAge: 40, courtesyTitle: 'MS', email: 'marta@example.com' };
const leo = { name: 'Leo', lastName: 'Gil', requestedAge: 5, courtesyTitle: 'MISTER' };
// retry: 0 on purpose. A retried test books again.
describe('certification bookings (test account)', { timeout: 300_000, retry: 0 }, () => {
it.each([
['1 room, 2 adults', [{ persons: [ana, luis] }]],
['2 rooms', [{ persons: [ana, luis] }, { persons: [omar, sara] }]],
['adults and children', [{ persons: [ana, luis] }, { persons: [marta, leo] }]],
] as [string, Room[]][])('%s', async (_name, rooms) => {
const orderId = `CERT-${Date.now()}`; // in production, save it before Book
const { token, booked } = await bookFlow(rooms, orderId);
// Test suppliers may not answer like real ones. If this fails, keep the output and
// ask your Nava account manager how the test suppliers respond.
expect(outcome(booked.status), `Book returned ${booked.status}`).toBe('confirmed');
expect(booked.distributions).toHaveLength(rooms.length);
// Read it back with booking detail (GET). Don't use Refresh here: it calls the supplier.
const detail = await call(token, 'GET',
`/booking/${booked.bookingReference}/accommodations/${booked.accommodation.bookingReference}`);
expect(detail.externalReference).toBe(orderId);
});
it('sends a failed Book down the failure path', async () => {
// fakeBooking is not saved, so make no post-booking calls on it.
const { booked } = await bookFlow([{ persons: [ana, luis] }], `CERT-FAIL-${Date.now()}`, { status: 'BOOK_ERROR' });
expect(outcome(booked.status)).toBe('failed'); // BOOK_ERROR or NOT_BOOKED at booking level
});
});Keep the three bookings: they are your evidence. Their traceId lines in the test output are the logs the review asks for.
Go-live checklist
Tick every item before you switch to production credentials.
- One auth token covers each whole booking flow, from Quote to Book, and a flow only starts when the token has at least 60 minutes left.
- Every step sends the
combinationKeyfrom the immediately preceding response. - Book and Cancel are never retried automatically.
externalReferenceis saved before Book, and a timeout starts reconciliation. - Your code branches on every Book status (
BOOKED,RQ,PRICE_ERROR,BOOK_ERROR,NOT_BOOKED,PENDING_BOOK) and sends any other value to review, never to success. - Every call sends
auth-tokenandAccept-Encoding: gzip, plusContent-Type: application/jsonwhen it has a body. -
auditData.authTokenis redacted before any response is logged or stored. -
combinationKeys stay on your server. Browsers and apps only see an id of your own. - Passenger names are transliterated to Latin script before Prebook, and the other field rules are checked client-side.
-
warnings[]from Confirm and Prebook are shown to the guest, and the flow continues only with their consent. - A webhook is set up for every microsite you use, in the back office under Data > Webhooks, tested with "Try Notification" and protected by a long random secret in the URL. It answers every
typewith a 2xx. - Personal data and the webhook secret are redacted in logs and analytics.
- HTTP timeouts are at least the Quote
timeoutplus 30 seconds for quotes, and at least 120 seconds for Book and Cancel. - A scheduled reconciliation catches bookings whose webhook never arrived. See Reading bookings.
- The
traceIdandx-request-idof every call are logged, and the full request and response of calls without a trace id. - No production code path sends
fakeBooking. A fake booking is not saved or sent to the supplier, even in production. - Production credentials come from environment variables, kept apart from your test credentials.
NAVA_BASE_URLishttps://nava.travel/resources, andNAVA_ENVis nottestin production.
Related
FAQ
Short answers to the questions agencies ask most about search, rooms and guests, booking, hotel content and commercial terms, plus the passenger validation errors the API returns and how to prevent them.
Loading hotel contracts
For suppliers with direct hotel contracts. Push hotels, rooms, meal plans, seasons, rates, offers and supplements into the platform, which then sells them through the API alongside other providers.