Payments¶
HGN hosts the payment page. You initiate a payment, redirect the customer to the URL you get back, and read the result when they return.
sequenceDiagram
autonumber
participant P as Your backend
participant A as HGN API
participant C as Customer
participant G as Gateway
P->>A: POST /orders/{orderRef}/pay
A-->>P: paymentUrl
P-->>C: redirect to paymentUrl
C->>G: completes payment
G->>A: callback to /api/external/payment/response
A-->>C: redirect to returnUrl?paymentRef=...
C->>P: lands on your returnUrl
P->>A: GET /orders/{orderRef}/payment-status
A-->>P: authoritative status
Never trust the redirect as proof of payment
The customer landing back on your returnUrl means only that their browser was redirected. It is not proof that the payment succeeded — the redirect can be replayed, interrupted, or forged.
Always confirm with GET /orders/{orderRef}/payment-status server-side before fulfilling.
POST /orders/{orderRef}/pay¶
Starts a hosted online payment and returns the URL to send the customer to.
Authentication: required.
Path parameters¶
| Name | Type | Description |
|---|---|---|
orderRef |
String | Any identifier that resolves to a single order — see resolving order identifiers |
Headers¶
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <accessToken> |
Idempotency-Key |
Yes | A UUID. Non-UUID values are rejected before anything is charged |
Content-Type |
Yes | application/json |
Request body¶
| Field | Type | Required | Validation |
|---|---|---|---|
gateway |
String | Yes | cybersource or fonepay. cash is rejected |
returnUrl |
String | No | Must be an HTTPS URL |
Response¶
200 OK
{
"success": true,
"message": "Payment initiated",
"data": {
"paymentUrl": "https://pay.himalayanguardian.com/pay/session/3f7a1b2c-...",
"gateway": "fonepay"
},
"timestamp": "2026-07-07T03:30:00Z"
}
Redirect the customer to paymentUrl.
Examples¶
import { randomUUID } from "node:crypto";
// Persist this key against the order BEFORE calling, so a retry
// after a timeout reuses the same key rather than generating a new one.
const idempotencyKey = randomUUID();
const res = await fetch(`${BASE_URL}/orders/${orderRef}/pay`, {
method: "POST",
headers: {
Authorization: `Bearer ${await getAccessToken()}`,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
gateway: "fonepay",
returnUrl: "https://partner.example.com/payment-result",
}),
});
const { data } = await res.json();
redirect(data.paymentUrl);
import uuid
idempotency_key = str(uuid.uuid4()) # persist against the order first
res = requests.post(
f"{BASE_URL}/orders/{order_ref}/pay",
headers={
"Authorization": f"Bearer {access_token}",
"Idempotency-Key": idempotency_key,
},
json={
"gateway": "fonepay",
"returnUrl": "https://partner.example.com/payment-result",
},
timeout=15,
)
res.raise_for_status()
payment_url = res.json()["data"]["paymentUrl"]
Idempotency¶
Idempotency-Key is mandatory and must be a valid UUID. A malformed value fails validation before the payment service is reached.
Generate the key once, per payment attempt
Store the key alongside the order before you make the call. If the request times out and you retry with a freshly generated key, you are making a new payment attempt rather than resuming the previous one.
Errors¶
| Status | Cause |
|---|---|
400 |
gateway is cash; invalid or non-HTTPS returnUrl; the order is already paid, closed, completed, or cancelled; malformed UUID in Idempotency-Key; or the identifier is a payment reference covering a multi-order group payment |
401 |
Missing or invalid token |
404 |
No order matches the identifier |
Online gateways only
This endpoint initiates hosted online payments. gateway: "cash" is explicitly rejected — cash settlement is recorded through the order's settlementType, not here.
GET /orders/{orderRef}/payment-status¶
Returns the most recent payment transaction for an order. This is the authoritative source of truth.
Authentication: required.
Path parameters¶
| Name | Type | Description |
|---|---|---|
orderRef |
String | Any identifier that resolves to a single order — see below |
Response¶
200 OK — two shapes, depending on whether a payment has been attempted.
{
"success": true,
"message": "No payment transaction found",
"data": {
"orderRef": "ORD-2026-000058",
"orderStatus": "PENDING",
"paymentStatus": "NOT_INITIATED"
},
"timestamp": "2026-07-07T03:30:00Z"
}
Note this is still 200 OK with success: true — "no payment yet" is a valid state, not an error.
{
"success": true,
"message": "Payment status retrieved",
"data": {
"orderRef": "ORD-2026-000058",
"orderStatus": "PAID",
"paymentRef": "PAY-2026-000001",
"paymentStatus": "SUCCESS",
"paymentMethod": "CYBERSOURCE",
"amount": 590.00,
"currency": "NPR",
"completedAt": "2026-07-07T03:30:00Z"
},
"timestamp": "2026-07-07T03:30:00Z"
}
| Field | Type | Description |
|---|---|---|
orderRef |
String | The resolved order reference |
orderStatus |
String | Order state, e.g. PENDING, PAID |
paymentStatus |
String | Payment state, e.g. NOT_INITIATED, SUCCESS |
paymentRef |
String | Payment record reference. Absent when no payment exists |
paymentMethod |
String | Gateway used, e.g. CYBERSOURCE |
amount |
Decimal | Amount transacted |
currency |
String | Currency code |
completedAt |
Timestamp | When the payment completed |
Guard on the field, not the shape
The lean "no payment yet" response omits paymentRef, amount, currency, and completedAt entirely. Read paymentStatus first and only reach for the other fields once it is something other than NOT_INITIATED.
Errors¶
| Status | Cause |
|---|---|
400 |
The identifier is a payment reference covering a multi-order group payment, so it does not resolve to a single order |
401 |
Missing or invalid token |
403 |
Your company does not own this order |
404 |
No order matches the identifier |
Resolving order identifiers¶
Both payment endpoints accept any identifier that resolves to exactly one order:
| Identifier | Example |
|---|---|
| Order reference | ORD-2026-000058 |
| Order number | ORDN-2026-000058 |
| Order UUID | 3f7a1b2c-... |
| Gateway payment reference | ORD-2026-000247-1786524461302 |
The paymentRef trap¶
When the customer returns to your returnUrl, HGN appends a paymentRef query parameter:
paymentRef is not an order reference
Its shape is <orderRef>-<epochMillis>. It looks like an order reference with a suffix, and it is accepted by both payment endpoints — it resolves back to the order it paid for.
But do not store it as an order reference, and do not try to parse the order reference out of it by splitting on the last hyphen. Use it as an opaque lookup key, or record the real orderRef from the order you created.
One case fails: if the payment reference covers a payment across a multi-order group, it cannot resolve to a single order and both endpoints return 400. Query each order's own reference instead.
Gateway callbacks¶
The payment gateway posts its browser callback to a separate backend path:
This endpoint exists for CyberSource and browser redirects. Partner systems never call it directly — you only ever see its effect, as the redirect that lands the customer back on your returnUrl.
returnUrl validation¶
returnUrl must be an HTTPS URL. Plain HTTP is rejected outside of HGN's internal test environment, so use HTTPS even in your own staging setup.
Recommended flow¶
- Create the order and store its
orderRef. - Generate and persist an
Idempotency-Keyagainst that order. POST /orders/{orderRef}/paywith areturnUrlyou control.- Redirect the customer to
paymentUrl. - When they return, ignore the query string as evidence and call
GET /orders/{orderRef}/payment-status. - Fulfil only when
paymentStatusis a success state.
Handle customers who never come back
Customers close tabs mid-payment. Do not rely solely on the redirect to trigger reconciliation — poll payment-status for orders left in PENDING, or reconcile on a schedule, so a completed payment is never missed because the browser never returned.
Next steps¶
- Promo Codes — discounts must be applied before payment
- Errors — every error shape this API returns
- Examples — the full flow end to end