Examples¶
A complete booking, end to end, in the order you would actually make the calls.
Setup¶
Store credentials as environment variables — never in source control.
export HGN_BASE_URL="https://api.himalayanguardian.com/api/external/v1"
export HGN_CLIENT_ID="hgn_ext_example"
export HGN_CLIENT_SECRET="secret-value"
The full flow with cURL¶
1. Get a token¶
TOKEN=$(curl -s -X POST "$HGN_BASE_URL/auth/token" \
-H "Content-Type: application/json" \
-d "{\"clientId\":\"$HGN_CLIENT_ID\",\"clientSecret\":\"$HGN_CLIENT_SECRET\"}" \
| jq -r '.data.accessToken')
2. Find a route¶
curl -s -G "$HGN_BASE_URL/reference/routes" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "search=Annapurna" | jq '.data.content[0]'
3. Get a quote¶
QUOTE=$(curl -s -X POST "$HGN_BASE_URL/reference/quote" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nationality": "American",
"ages": [28],
"routeId": "550e8400-e29b-41d4-a716-446655440000",
"startDate": "2026-10-01",
"duration": 14
}')
echo "$QUOTE" | jq '.data | {ctgId, ctgName, totalAmount}'
4. Upload the three required documents¶
upload() {
curl -s -X POST "$HGN_BASE_URL/documents" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@$1" \
-F "documentType=$2" | jq -r '.data.documentId'
}
AVATAR_ID=$(upload avatar.jpg AVATAR)
PASSPORT_ID=$(upload passport.pdf PASSPORT)
SIGNATURE_ID=$(upload signature.png SIGNATURE)
5. Register the traveller¶
TOURIST_ID=$(curl -s -X POST "$HGN_BASE_URL/travellers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "[{
\"email\": \"john@example.com\",
\"name\": \"John Doe\",
\"dateOfBirth\": \"1990-04-15\",
\"nationality\": \"US\",
\"idType\": \"PASSPORT\",
\"idNumber\": \"A12345678\",
\"gender\": \"MALE\",
\"phone\": \"+9779812345678\",
\"emergencyContactName\": \"Jane Doe\",
\"emergencyContactPhone\": \"+9779812345679\",
\"emergencyContactEmail\": \"jane@example.com\",
\"emergencyContactRelation\": \"Spouse\",
\"avatarDocumentId\": \"$AVATAR_ID\",
\"passportDocumentId\": \"$PASSPORT_ID\",
\"signatureDocumentId\": \"$SIGNATURE_ID\"
}]" | jq -r '.data[0].touristId')
6. Create the order¶
CTG_ID=$(echo "$QUOTE" | jq -r '.data.ctgId')
QUOTE_ID=$(echo "$QUOTE" | jq -r '.data.quoteId')
ORDER=$(curl -s -X POST "$HGN_BASE_URL/orders" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"touristId\": \"$TOURIST_ID\",
\"ctgId\": \"$CTG_ID\",
\"routeId\": \"550e8400-e29b-41d4-a716-446655440000\",
\"departureDate\": \"2026-10-01\",
\"quoteId\": \"$QUOTE_ID\"
}")
7. Start payment¶
ORDER_REF="ORD-2026-000058"
curl -s -X POST "$HGN_BASE_URL/orders/$ORDER_REF/pay" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"gateway": "fonepay",
"returnUrl": "https://partner.example.com/payment-result"
}' | jq -r '.data.paymentUrl'
8. Confirm¶
curl -s "$HGN_BASE_URL/orders/$ORDER_REF/payment-status" \
-H "Authorization: Bearer $TOKEN" | jq '.data'
A complete Node.js client¶
hgn-client.mjs
import { randomUUID } from "node:crypto";
import { openAsBlob } from "node:fs";
const BASE_URL = process.env.HGN_BASE_URL;
// --- Token cache -----------------------------------------------------------
let cached = { token: null, expiresAt: 0 };
async function getAccessToken() {
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;
}
// --- Request helper --------------------------------------------------------
async function api(path, { method = "GET", body, headers = {} } = {}) {
const send = async (token) =>
fetch(`${BASE_URL}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
...(body instanceof FormData
? {}
: { "Content-Type": "application/json" }),
...headers,
},
body: body instanceof FormData ? body : body && JSON.stringify(body),
});
let res = await send(await getAccessToken());
// A token can be revoked mid-flight. Refresh and retry exactly once.
if (res.status === 401) {
cached = { token: null, expiresAt: 0 };
res = await send(await getAccessToken());
}
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw Object.assign(new Error(err.message ?? res.statusText), {
status: res.status,
fieldErrors: err.details?.fieldErrors ?? {},
});
}
return (await res.json()).data;
}
// --- Booking flow ----------------------------------------------------------
async function uploadDocument(path, documentType) {
const form = new FormData();
form.append("file", await openAsBlob(path), path.split("/").pop());
form.append("documentType", documentType);
const data = await api("/documents", { method: "POST", body: form });
return data.documentId;
}
export async function bookTrip({ traveller, routeId, startDate, duration }) {
// 1. Price the trip.
const quote = await api("/reference/quote", {
method: "POST",
body: {
nationality: traveller.nationality,
ages: [traveller.age],
routeId,
startDate,
duration,
},
});
// 2. Documents and traveller. All three document types are mandatory.
const [avatarId, passportId, signatureId] = await Promise.all([
uploadDocument(traveller.avatarPath, "AVATAR"),
uploadDocument(traveller.passportPath, "PASSPORT"),
uploadDocument(traveller.signaturePath, "SIGNATURE"),
]);
const [created] = await api("/travellers", {
method: "POST",
body: [
{
...traveller.details,
avatarDocumentId: avatarId,
passportDocumentId: passportId,
signatureDocumentId: signatureId,
},
],
});
// 3. Order. Echo the quote's own ids straight back.
const order = await api("/orders", {
method: "POST",
body: {
touristId: created.touristId,
ctgId: quote.ctgId,
routeId: quote.routeId,
departureDate: quote.startDate,
quoteId: quote.quoteId,
},
});
return { quote, order };
}
export async function startPayment(orderRef, returnUrl, idempotencyKey) {
// Persist idempotencyKey against the order BEFORE calling, and reuse the
// same value on any retry — a new key starts a new payment attempt.
return api(`/orders/${orderRef}/pay`, {
method: "POST",
headers: { "Idempotency-Key": idempotencyKey ?? randomUUID() },
body: { gateway: "fonepay", returnUrl },
});
}
export async function confirmPayment(orderRef) {
const status = await api(`/orders/${orderRef}/payment-status`);
return {
paid: status.paymentStatus === "SUCCESS",
status,
};
}
A complete Python client¶
hgn_client.py
import os
import time
import uuid
import requests
BASE_URL = os.environ["HGN_BASE_URL"]
class HgnApiError(Exception):
def __init__(self, status, message, field_errors=None):
super().__init__(f"[{status}] {message}")
self.status = status
self.message = message
self.field_errors = field_errors or {}
class HgnClient:
def __init__(self, client_id=None, client_secret=None):
self.client_id = client_id or os.environ["HGN_CLIENT_ID"]
self.client_secret = client_secret or os.environ["HGN_CLIENT_SECRET"]
self._token = None
self._expires_at = 0
self._session = requests.Session()
# --- Auth -------------------------------------------------------------
def _access_token(self):
# Refresh 60s early to absorb clock skew and in-flight latency.
if self._token and time.time() < self._expires_at - 60:
return self._token
res = self._session.post(
f"{BASE_URL}/auth/token",
json={"clientId": self.client_id, "clientSecret": self.client_secret},
timeout=10,
)
res.raise_for_status()
data = res.json()["data"]
self._token = data["accessToken"]
self._expires_at = time.time() + data["expiresIn"]
return self._token
# --- Request helper ---------------------------------------------------
def _request(self, method, path, _retried=False, **kwargs):
res = self._session.request(
method,
f"{BASE_URL}{path}",
headers={
"Authorization": f"Bearer {self._access_token()}",
**kwargs.pop("headers", {}),
},
timeout=kwargs.pop("timeout", 15),
**kwargs,
)
if res.status_code == 401 and not _retried:
self._token = None # force refresh, retry once
return self._request(method, path, _retried=True, **kwargs)
if not res.ok:
try:
body = res.json()
except ValueError:
raise HgnApiError(res.status_code, res.reason)
raise HgnApiError(
res.status_code,
body.get("message", res.reason),
(body.get("details") or {}).get("fieldErrors", {}),
)
return res.json()["data"]
# --- Booking flow -----------------------------------------------------
def quote(self, nationality, age, route_id, start_date, duration):
return self._request(
"POST",
"/reference/quote",
json={
"nationality": nationality,
"ages": [age],
"routeId": route_id,
"startDate": start_date,
"duration": duration,
},
)
def upload_document(self, path, document_type):
with open(path, "rb") as fh:
data = self._request(
"POST",
"/documents",
files={"file": fh},
data={"documentType": document_type},
timeout=30,
)
return data["documentId"]
def create_traveller(self, details, avatar_id, passport_id, signature_id):
created = self._request(
"POST",
"/travellers",
json=[
{
**details,
"avatarDocumentId": avatar_id,
"passportDocumentId": passport_id,
"signatureDocumentId": signature_id,
}
],
)
return created[0]
def create_order(self, tourist_id, quote):
return self._request(
"POST",
"/orders",
json={
"touristId": tourist_id,
"ctgId": quote["ctgId"],
"routeId": quote["routeId"],
"departureDate": quote["startDate"],
"quoteId": quote["quoteId"],
},
)
def start_payment(self, order_ref, return_url, idempotency_key=None):
# Persist the key against the order first; reuse it on every retry.
key = idempotency_key or str(uuid.uuid4())
return self._request(
"POST",
f"/orders/{order_ref}/pay",
headers={"Idempotency-Key": key},
json={"gateway": "fonepay", "returnUrl": return_url},
)
def payment_status(self, order_ref):
return self._request("GET", f"/orders/{order_ref}/payment-status")
Using it¶
client = HgnClient()
quote = client.quote(
nationality="American",
age=28,
route_id="550e8400-e29b-41d4-a716-446655440000",
start_date="2026-10-01",
duration=14,
)
print(f"{quote['ctgName']}: {quote['totalAmount']}")
avatar = client.upload_document("avatar.jpg", "AVATAR")
passport = client.upload_document("passport.pdf", "PASSPORT")
signature = client.upload_document("signature.png", "SIGNATURE")
traveller = client.create_traveller(
{
"email": "john@example.com",
"name": "John Doe",
"dateOfBirth": "1990-04-15",
"nationality": "US",
"idType": "PASSPORT",
"idNumber": "A12345678",
"gender": "MALE",
"phone": "+9779812345678",
"emergencyContactName": "Jane Doe",
"emergencyContactPhone": "+9779812345679",
"emergencyContactEmail": "jane@example.com",
"emergencyContactRelation": "Spouse",
},
avatar,
passport,
signature,
)
order = client.create_order(traveller["touristId"], quote)
payment = client.start_payment(
order["orderRef"], "https://partner.example.com/payment-result"
)
print("Send the customer to:", payment["paymentUrl"])
Recipes¶
Price a group¶
A quote covers one traveller, so quote each person and sum.
const ages = [28, 34, 41];
const quotes = await Promise.all(
ages.map((age) =>
api("/reference/quote", {
method: "POST",
body: { nationality: "American", ages: [age], routeId, startDate, duration },
})
)
);
const total = quotes.reduce((sum, q) => sum + Number(q.totalAmount), 0);
Create a batch of orders¶
Send an array to group them, then use the group reference for batch promo operations.
const group = await api("/orders", {
method: "POST",
body: travellers.map((t) => ({
touristId: t.touristId,
ctgId: quote.ctgId,
routeId: quote.routeId,
departureDate: quote.startDate,
})),
});
Apply a discount before payment¶
// Show the customer what it saves, without committing.
const preview = await api(`/orders/${orderRef}/promo/preview`, {
method: "POST",
body: { promoCode: "HGN-SAVE10" },
});
// Then apply it. A 400 means "not usable" — the reason is deliberately opaque.
try {
await api(`/orders/${orderRef}/promo`, {
method: "POST",
body: { promoCode: "HGN-SAVE10" },
});
} catch (err) {
if (err.status === 400) showMessage("That promo code cannot be used.");
else throw err;
}
Drop a bundled device¶
const unwanted = quote.includedDevices.find((d) => d.name === "Tracer M3");
await api("/orders", {
method: "POST",
body: {
touristId,
ctgId: quote.ctgId,
routeId: quote.routeId,
departureDate: quote.startDate,
quoteId: quote.quoteId,
excludedIncludedDeviceIds: [unwanted.deviceId],
},
});
Book an uncatalogued route¶
await api("/orders", {
method: "POST",
body: {
touristId,
ctgId: quote.ctgId,
customRouteName: "Private Manaslu Variant", // instead of routeId
departureDate: "2026-10-01",
},
});
Reconcile abandoned payments¶
Customers close tabs. Poll orders left pending rather than relying on the redirect.
def reconcile(client, order_refs):
"""Re-check orders that never came back through the return URL."""
settled = []
for ref in order_refs:
status = client.payment_status(ref)
if status["paymentStatus"] != "NOT_INITIATED":
settled.append((ref, status["paymentStatus"]))
return settled
Handling the return URL¶
The customer comes back to your returnUrl with a paymentRef query parameter:
app.get("/payment-result", async (req, res) => {
const { paymentRef } = req.query;
// The redirect is NOT proof of payment. Confirm server-side.
// paymentRef is an opaque lookup key — do not parse an orderRef out of it.
const status = await api(`/orders/${paymentRef}/payment-status`);
if (status.paymentStatus === "SUCCESS") {
await fulfil(status.orderRef);
return res.render("success", { orderRef: status.orderRef });
}
return res.render("pending", { orderRef: status.orderRef });
});
Batch payments
If the payment covered a multi-order group, paymentRef cannot resolve to a single order and this call returns 400. Query each order's own reference instead — see Payments.