Promo Codes¶
Promo codes discount an order after it has been created but before it is paid. There are three operations — preview, apply, and remove — available for a single order and for a whole batch.
Unpaid orders only
Apply and remove require the order to still be pending and unpaid. Preview works on any order. Once payment succeeds, the discount can no longer be changed. Apply codes between creating the order and starting payment.
How codes work¶
Two slots per order¶
An order holds at most one sales-company code and one sales-agent code — two independent slots.
| Slot | Holds |
|---|---|
| Sales company | One company promo code |
| Sales agent | One agent promo code |
Applying a code replaces whichever slot its type maps to and leaves the other untouched. Using both types on one order therefore means two requests.
You never send the code's type¶
Promo codes are globally unique, so HGN resolves the type — company or agent — from the code itself. There is no type parameter, and one request carries exactly one code.
Binding¶
Applying a code to an empty slot binds the order to that code's owner, which constrains what the order will accept afterwards:
| Order state | Company code accepted | Agent code accepted |
|---|---|---|
| No binding | Any company's | Any agent's |
| Bound to company A | A's only | Any agent's |
| Bound to agent X | Any company's | X's only |
Removing a code rolls its binding back, freeing the slot again.
Preview a code¶
Shows what a code would do, without changing the order. Use it to display a discount to the customer before they commit.
POST /api/external/v1/orders/{orderRef}/promo/preview
Authorization: Bearer <accessToken>
Content-Type: application/json
Response¶
200 OK — the codes the order would carry, the projected discount, and the totals before and after.
| Field | Type | Description |
|---|---|---|
orderRef |
String | The order |
appliedPromoCodes |
Object[] | Codes on the order (or that would be, for preview) |
discountAmount |
Decimal | Total promo discount |
amountBefore |
Decimal | Order total before the discount |
amountAfter |
Decimal | Order total after the discount |
A blank or missing promoCode on preview is not an error: it returns the order's current promo state.
curl -X POST \
https://apitest.himalayanguardian.com/api/external/v1/orders/ORD-2026-000058/promo/preview \
-H "Authorization: Bearer $HGN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"promoCode": "HGN-SAVE10"}'
Preview never mutates the order, so it is safe to call on every keystroke of a promo-code field — though debouncing is still polite.
Apply a code¶
Applies one code to an unpaid order.
POST /api/external/v1/orders/{orderRef}/promo
Authorization: Bearer <accessToken>
Content-Type: application/json
Response¶
200 OK — the same payload shape as preview, reflecting the now-applied state.
A rejected code returns 400 and leaves the order completely untouched — there is no partial application to clean up. Apply is not idempotent: sending a code that is already on the order returns 400 "Promo code … is already applied to this order".
async function applyPromo(orderRef, promoCode, token) {
const res = await fetch(`${BASE_URL}/orders/${orderRef}/promo`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ promoCode }),
});
if (res.status === 400) {
// Code-validity failures all read "Invalid promo code"; show a generic message.
return { applied: false, reason: "Promo code could not be applied" };
}
if (!res.ok) throw new Error(`Promo failed: ${res.status}`);
const { data } = await res.json();
return { applied: true, data };
}
def apply_promo(order_ref, promo_code, access_token):
res = requests.post(
f"{BASE_URL}/orders/{order_ref}/promo",
headers={"Authorization": f"Bearer {access_token}"},
json={"promoCode": promo_code},
timeout=10,
)
if res.status_code == 400:
return {"applied": False, "reason": "Invalid promo code"}
res.raise_for_status()
return {"applied": True, "data": res.json()["data"]}
Applying both code types¶
// Two separate requests — one per slot.
await applyPromo(orderRef, "COMPANY-CODE", token);
await applyPromo(orderRef, "AGENT-CODE", token);
Remove a code¶
| Query parameter | Type | Required | Description |
|---|---|---|---|
code |
String | No | Clears the slot this code currently occupies. Omit to clear both slots |
# Remove one specific code
curl -X DELETE \
"https://apitest.himalayanguardian.com/api/external/v1/orders/ORD-2026-000058/promo?code=HGN-SAVE10" \
-H "Authorization: Bearer $HGN_TOKEN"
# Remove every applied code
curl -X DELETE \
"https://apitest.himalayanguardian.com/api/external/v1/orders/ORD-2026-000058/promo" \
-H "Authorization: Bearer $HGN_TOKEN"
Removal is safe to repeat: a code that is not applied to the order is a no-op that returns the current state rather than an error.
Clearing a code reverses its usage reservation — returning it to the pool — and rolls back any binding it created.
Batch orders¶
A batch create returns a group reference. The same three operations exist at the group level:
| Method | Endpoint |
|---|---|
POST |
/orders/groups/{groupRef}/promo/preview |
POST |
/orders/groups/{groupRef}/promo |
DELETE |
/orders/groups/{groupRef}/promo |
They take the same body and query parameters as their single-order counterparts, apply one code to every eligible order in the batch, and return one entry per order plus group totals: groupRef, orders[] (one single-order response each), discountAmount, amountBefore, amountAfter.
Only orders in the group that are still pending and unpaid are included. Apply is partial: it returns 400 only if the code applied to none of them, or if the group has no eligible orders. An unknown groupRef returns 400 "Order group not found".
curl -X POST \
https://apitest.himalayanguardian.com/api/external/v1/orders/groups/GRP-2026-000012/promo \
-H "Authorization: Bearer $HGN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"promoCode": "HGN-SAVE10"}'
Read the per-order entries
Eligibility is evaluated per order, so a group response can be mixed — some orders discounted, others not. Do not treat a 200 as "applied to everything"; walk the per-order entries.
Promo codes at order creation¶
You can also pass promoCode directly on POST /orders. This is applied non-fatally:
- A rejected code does not fail the order — it is still created.
- The outcome is reported on
promoApplyResultin the create response.
These endpoints are how you recover from that: apply the code afterwards, or replace one the customer changed their mind about. Both require the order to still be pending and unpaid.
Errors¶
| Status | Cause |
|---|---|
400 |
Code not usable (Invalid promo code), blank promoCode on apply, code already applied to this order, order not pending and unpaid (apply/remove), or unknown groupRef |
401 |
Missing or invalid token |
403 |
Your company does not own this order |
404 |
No order matches the reference. {orderRef} accepts an order reference, order number or UUID — not a payment reference or group reference |
Code rejections share one message¶
Do not branch on the rejection message
Whatever makes a code unusable — unknown, expired, inactive, or owned by another party — the rejection reads Invalid promo code. This is deliberate: distinct messages would let a caller probe which codes exist.
Other 400s carry their own message (code already applied, missing code, order not pending, usage limit reached on a group preview). Treat every 400 from a promo endpoint as "code not usable" and surface a generic message to the customer.
Ownership¶
Promo operations resolve against the same company that created the order. You can only reach orders your own company placed — anything else returns 403.