Authentication¶
Every endpoint except POST /auth/token requires a bearer token. Tokens are short-lived and obtained by exchanging the clientId and clientSecret issued to you by HGN.
Credentials¶
HGN issues each partner a credential pair tied to a company account.
| Field | Description |
|---|---|
clientId |
Public identifier for your integration |
clientSecret |
Secret value. Treat it like a password |
Credential types¶
Your credential is bound to one of two company types, which changes how orders attribute commission:
| Type | Behaviour on POST /orders |
|---|---|
| Travel agency | travelCompanyIds is always replaced with your own company; any other IDs you send are dropped. customTravelAgencies is rejected |
| Sales company | Commission ownership resolves from your authenticated company context |
Protect your client secret
The secret authenticates as your company and can create real, billable orders.
- Store it in a secrets manager or server-side environment variable — never in source control.
- Never ship it to a browser, mobile app, or any client you do not control.
- If it is exposed, contact the HGN integrations team. Rotating the secret does not revoke tokens already issued — they stay valid until they expire (up to 1 hour). To cut off access immediately, ask HGN to deactivate the credential; that takes effect on the next request.
A credential can carry an expiry date set by HGN. After it passes, token requests fail with 400 "Credentials have expired". A company can hold at most 5 active credentials.
POST /auth/token¶
Exchanges partner credentials for a bearer token.
Authentication: none — this is the only unauthenticated endpoint.
Request¶
| Field | Type | Required | Validation |
|---|---|---|---|
clientId |
String | Yes | Non-blank |
clientSecret |
String | Yes | Non-blank |
Response¶
200 OK
{
"success": true,
"message": "Token issued",
"data": {
"accessToken": "k3F9qLm2Xw8ZrT4vBn6YcP1sHd7GjE0uAo5KiQ2tRw9NbV3x",
"tokenType": "Bearer",
"expiresIn": 3600
},
"timestamp": "2026-07-07T03:30:00Z"
}
| Field | Type | Description |
|---|---|---|
accessToken |
String | The bearer token to send on every other request. An opaque 48-character string; do not parse it |
tokenType |
String | Always Bearer |
expiresIn |
Long | Token lifetime in seconds |
Examples¶
const res = await fetch(
"https://apitest.himalayanguardian.com/api/external/v1/auth/token",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: process.env.HGN_CLIENT_ID,
clientSecret: process.env.HGN_CLIENT_SECRET,
}),
}
);
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
const { data } = await res.json();
const accessToken = data.accessToken;
$ch = curl_init('https://apitest.himalayanguardian.com/api/external/v1/auth/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'clientId' => getenv('HGN_CLIENT_ID'),
'clientSecret' => getenv('HGN_CLIENT_SECRET'),
]),
]);
$body = json_decode(curl_exec($ch), true);
$accessToken = $body['data']['accessToken'];
Errors¶
This endpoint never returns 401. Every failure is a 400 in the validation/business error shape:
| Status | Cause |
|---|---|
400 |
clientId or clientSecret missing (with details.fieldErrors), unknown or deactivated client, wrong secret (Invalid credentials), expired credential (Credentials have expired), or the owning company is inactive |
429 |
Too many token requests. This endpoint is limited per IP, at half the normal rate (150 per minute by default). Honour Retry-After |
Cache the token
Request a token once and reuse it until shortly before expiresIn runs out. Requesting one per call will hit the token rate limit.
Using the token¶
Send the token as a bearer credential on every other /api/external/v1/** request.
The token is opaque — do not parse it
accessToken is an unguessable random string validated server-side on every request. It is not a JWT. It has no readable claims, no embedded expiry, and no signature you can verify.
Do not decode it, do not inspect it, and do not derive an expiry from it. Track expiry using the expiresIn value returned alongside it.
Token lifecycle¶
Tokens expire after expiresIn seconds. There is no refresh token — when a token expires, call POST /auth/token again.
The recommended pattern is to cache the token in your backend and refresh it slightly early, so an in-flight request never lands on a just-expired token.
let cached = { token: null, expiresAt: 0 };
async function getAccessToken() {
// Refresh 60s before actual expiry to absorb clock skew and latency.
if (cached.token && Date.now() < cached.expiresAt - 60_000) {
return cached.token;
}
const res = await fetch(`${BASE_URL}/auth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: process.env.HGN_CLIENT_ID,
clientSecret: process.env.HGN_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`Auth failed: ${res.status}`);
const { data } = await res.json();
cached = {
token: data.accessToken,
expiresAt: Date.now() + data.expiresIn * 1000,
};
return cached.token;
}
Handle 401 as a retry, once
Even with early refresh, a token can be revoked mid-flight. On a 401, discard your cached token, fetch a new one, and retry the request once. If the retry also returns 401, the credential itself is the problem — stop and alert, rather than looping.
Authentication failures¶
A request is rejected with 401 Unauthorized when:
- the
Authorizationheader is missing, or not in the exact formBearer <token>(case-sensitive, one space) - the token is unknown, expired, or revoked
- the credential has been revoked or deleted
- the company that owns the credential is inactive or deleted
These rejections happen before your request reaches the endpoint, so they use a reduced envelope with no timestamp field:
Distinguishing causes
All four conditions return 401. The message differs but is not a stable contract — do not branch on it. If tokens suddenly stop working across all your integrations at once, suspect a revoked credential or a deactivated company account and contact HGN.
See Errors for the other error shapes this API can return.
Next steps¶
- Availability — price a trip and get a
quoteId - Examples — a complete booking from token to payment