Skip to content

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:

https://partner.example.com/payment-result?paymentRef=ORD-2026-000247-1786524461302
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.


Next steps

  • Overview — conventions and the full endpoint list
  • Errors — all three error shapes and how to normalise them