Authentication
Exchange your credentials for a token that lasts two hours, send it in the auth-token header, and use the same token for a whole booking flow.
Every call except POST /authentication/authenticate needs a token in the auth-token header. How you manage that token depends on the job: a booking flow pins one token from Quote to Book, while catalogue and back-office jobs can share a cached one.
Get a token
| Body field | Type | |
|---|---|---|
username | string, required | Your API user. |
password | string, required | Your API password. |
micrositeId | string, required | The microsite the credentials belong to. Your test credentials come with their own test microsite id. |
All three come from your Nava account manager. See Environments and credentials.
curl -s --compressed -X POST "$NAVA_BASE_URL/authentication/authenticate" \
-H 'Content-Type: application/json' \
-H 'Accept-Encoding: gzip' \
-d '{
"username": "'"$NAVA_USERNAME"'",
"password": "'"$NAVA_PASSWORD"'",
"micrositeId": "'"$NAVA_MICROSITE_ID"'"
}'{
"token": "eyJhbGciOiJIUzUxMiJ9.…",
"expirationInSeconds": 7200
}The token is a JWT and lasts 7200 seconds (two hours). Use expirationInSeconds from the response rather than hard-coding the lifetime.
Send the token
Send the token in a custom auth-token header on every call. It is not an Authorization: Bearer header.
GET /resources/accommodations?first=0&limit=100 HTTP/1.1
Host: nava.travel
auth-token: eyJhbGciOiJIUzUxMiJ9.…
Accept-Encoding: gzipEvery response echoes the token back in auditData.authToken. Redact it before you log or store a response. See Requests and responses.
One token per booking flow
A booking flow (Quote, Quote single, Confirm, Prebook, Book) must use the same token from the first call to the last. A new token mid-flow means starting again from Quote.
If a flow outlives its token anyway, the next step fails. Get a new token, re-quote the same hotel with Quote single, and match the same room, meal plan and refundability. If the price differs, show the guest the new price and get consent again.
The helper below keeps one cached token, hands a flow a token only when it has at least 60 minutes left, and never refreshes a token a flow is using.
// One token per booking flow; one cached token for everything else.
import { NavaApiError } from './nava-client'; // see Requests and responses
const BASE = process.env.NAVA_BASE_URL!;
const FLOW_MIN_MINUTES = 60;
export type Token = { value: string; expiresAt: number }; // expiresAt: epoch ms
let cached: Token | undefined;
async function authenticate(): Promise<Token> {
const res = await fetch(`${BASE}/authentication/authenticate`, {
method: 'POST',
headers: { 'Accept-Encoding': 'gzip', 'Content-Type': 'application/json' },
body: JSON.stringify({
username: process.env.NAVA_USERNAME,
password: process.env.NAVA_PASSWORD,
micrositeId: process.env.NAVA_MICROSITE_ID,
}),
signal: AbortSignal.timeout(20_000),
});
if (!res.ok) {
// 401: wrong password OR deleted API user (identical responses). 400: missing micrositeId.
throw new NavaApiError('authenticate', res.status, res.headers.get('Travelc-Trace-Id') ?? undefined,
res.headers.get('x-request-id') ?? undefined, await res.text());
}
const { token, expirationInSeconds } = await res.json();
// Keep a 60 s safety margin on the stated lifetime.
return { value: token, expiresAt: Date.now() + (expirationInSeconds - 60) * 1000 };
}
const minutesLeft = (t: Token) => (t.expiresAt - Date.now()) / 60_000;
/** Cached token for static content, booking reads and back-office jobs. */
export async function getToken(minMinutesLeft = 5): Promise<Token> {
if (!cached || minutesLeft(cached) < minMinutesLeft) cached = await authenticate();
return cached;
}
/**
* Token for ONE booking flow. Pass it to every step from Quote to Book and never refresh it.
* If it expires mid-flow, start a new flow from Quote.
*/
export async function startFlow(): Promise<Token> {
const token = await getToken(FLOW_MIN_MINUTES);
if (minutesLeft(token) < FLOW_MIN_MINUTES) {
throw new Error(`Token has ${Math.floor(minutesLeft(token))} min left; a booking flow needs ${FLOW_MIN_MINUTES}`);
}
return token;
}
/** Outside a booking flow: one forced re-auth on 401/403, then alert and fail. */
export async function withCachedToken<T>(run: (token: string) => Promise<T>): Promise<T> {
try {
return await run((await getToken()).value);
} catch (err) {
if (!isAuthError(err)) throw err;
cached = undefined; // force one re-authentication
try {
return await run((await getToken()).value);
} catch (again) {
if (isAuthError(again)) await alertOps('Nava API returns 401/403 after re-auth: check the API user', again);
throw again;
}
}
}
const isAuthError = (e: unknown) => e instanceof NavaApiError && (e.status === 401 || e.status === 403);Tokens outside a booking flow
Static content (the hotel catalogue) doesn't need the flow's token: any valid token works. Post-booking calls and booking reads also run on any valid token, and we have seen back-office syncs run on a cached token in production. For long-running jobs such as a catalogue sync or a back-office booking sync:
- Cache one token per credential and refresh it before it expires.
- On a
401or403, force one re-authentication and retry once. If that fails too, stop and alert a person. - Never retry authentication in a loop. A deleted API user fails the same way every time.
This is different from a booking flow, where a new token means starting again from Quote.
One-time tokens for SSO
Returns a single-use token that signs an existing platform user in (single sign-on). Send your API token in auth-token and a body of userName (capital N) and micrositeId. It needs a special API user: ask your Nava account manager. See OTP.
When authentication fails
The API doesn't tell you why a login failed.
| What happened | Response |
|---|---|
| Wrong password | 401 |
| The API user was deleted | 401, byte-identical to a wrong password |
micrositeId missing from the body | 400 |
| Token valid, but the account isn't enabled for the endpoint called | 401 with User … not allowed to access here |
{
"error": ["User not authorized to access"],
"status": "UNAUTHORIZED"
}Name every failure in your logs and alerts with the operation and the HTTP status (for example authenticate 401, quote 401). A generic "unclassified error" hides the one signal you have.
Everything returns 401
We have seen a working integration start getting 401 on every call because its API user was deleted or rotated. If that happens:
Stop the retry storm. Let each job fail after its one forced re-authentication, and alert.
Read the error message. User not authorized to access comes from authenticate and means the credential was rejected. User … not allowed to access here comes from other calls and means the credential works but the account isn't enabled for that endpoint: a new password won't fix it.
Ask your Nava account manager whether the API user still exists, or whether its password was changed.
Update the credential in every deployed service that uses it, not only the one that alerted.
Replay the failed read and sync jobs from your own event store. A checkout that failed needs a new flow from Quote, on a new token.
Expect a brief second wave of failures. After you change environment variables on a hosting platform, old instances can still run a few jobs with the old credential. Retry those jobs too.
Related
Postman collection
Download a ready-made Postman collection for the whole accommodation flow, from authentication to cancellation, with scripts that chain every call and block bookings outside your test account.
Environments and credentials
The two base URLs, one for your test account and one for production, the credentials that go with each, what to expect from test suppliers, and what depends on your microsite.