Skip to content

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

Every promo operation requires the order to still be pending and unpaid. 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
{ "promoCode": "HGN-SAVE10" }

Response

200 OK — the codes the order would carry, the projected discount, and the totals before and after.

curl -X POST \
  https://api.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
{ "promoCode": "HGN-SAVE10" }

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.

curl -X POST \
  https://api.himalayanguardian.com/api/external/v1/orders/ORD-2026-000058/promo \
  -H "Authorization: Bearer $HGN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"promoCode": "HGN-SAVE10"}'
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) {
    // Every rejection reason returns this same status and message.
    return { applied: false, reason: "Invalid promo code" };
  }
  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

DELETE /api/external/v1/orders/{orderRef}/promo
Authorization: Bearer <accessToken>
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://api.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://api.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.

curl -X POST \
  https://api.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 promoApplyResult in 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 Unknown promo code, blank promoCode, or an order that is not pending and unpaid
401 Missing or invalid token
403 Your company does not own this order
404 No order matches the reference

Every rejection returns the same message

Do not branch on the rejection message

Whatever the cause — expired, inactive, usage limit reached, or owned by another party — a rejected code returns the same message: Invalid promo code.

This is deliberate. Distinct messages would let a caller probe the endpoint to discover which codes exist. Treat every 400 from a promo endpoint as a single "code not usable" outcome 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.


Next steps

  • Payments — take payment once the discount is applied
  • Errors — every error shape this API returns