Trade portal · API reference
HT Window Fashions Back to portal

Pricing API

Price made-to-measure blinds at your trade terms, from your own ordering system. Everything here returns both the list price and your net price, so your system can show either.

Base URL The host and port we gave you. Every path below is relative to it, so /dealer/quote means $BASE/dealer/quote.
Products Your contracted range only. Anything outside it is not listed and cannot be priced — see Products.
Access Your API key and your registered IP address. Requests from anywhere else do not reach us — see Authentication.
Scope Quoting only. This API does not place orders, hold stock, or return lead times per job — see What this API does not do.

Quick start

Three steps to your first price.

  1. Ask your account manager for an API key. We issue it and send it to you — there is no self-service sign-up.
  2. Store it on your server. It is a secret — see Authentication.
  3. Send a configured line to /dealer/quote.
curl -X POST "$BASE/dealer/quote" \
  -H "X-API-Key: $HTW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "blind_type_code": "P60",
    "fabric_code": "PS41G0001",
    "dimensions": { "width_mm": "1219.2", "height_mm": "1219.2" },
    "options": [],
    "quantity": 1
  }'

What comes back

{
  "list_unit_price": { "amount": "252.00", "currency": "USD" },
  "unit_price":      { "amount": "100.80", "currency": "USD" },
  "list_line_total": { "amount": "252.00", "currency": "USD" },
  "line_total":      { "amount": "100.80", "currency": "USD" },
  "currency": "USD",
  "breakdown": [
    { "label": "Base (pls41g) 48.00\"x48.00\"",
      "amount": { "amount": "252.00", "currency": "USD" } }
  ],
  "rate_card_version": "erp-2026.08.04-1546",
  "discount": { "account": "XX-000", "multiplier": "0.400",
                "percent_off": "60.0", "source": "erp" }
}

Illustrative figures. Your account and your multipliers are your own — call /dealer/me to see them.

Money is a string, not a number. Parse it as a decimal, not a float — 0.1 + 0.2 is not 0.3 in floating point, and these are prices.
The stock cut charge is applied for you. Every Simply Cordless shade on your account is cut to size from a stock blind, so StockCutCharge: YES is added to Simply Cordless lines automatically and appears as its own line in the breakdown. You do not need to send it. If a line genuinely is not cut, send {"name": "StockCutCharge", "value": "NO"} and that wins.

How the discount is applied

Each line of the breakdown is discounted and rounded to the cent separately, and those are then added — the same arithmetic your invoice uses. So an $89 blind with a $15 stock cut charge at ×0.405 comes to 36.05 + 6.08 = 42.13, not 42.12. Rounding is half-up. line_total applies the quantity inside each component, so use it for the order value rather than multiplying unit_price yourself.

Authentication

Two ways in, and the choice matters.

MethodHeaderUse it for
API keyX-API-Key: htw_live_… Your server calling ours. This is the one you want.
Session tokenAuthorization: Bearer … The trade portal itself. Expires after 14 days.
Never put an API key in a web page, mobile app, or anything a browser downloads. Anyone can read it there, and it buys at your trade prices. Keep it on your server, in an environment variable — not in source control. If one leaks, tell us and we revoke it; it stops working on the next request.

Keys are shown once, when issued. We store only a hash, so we cannot recover one for you — if it is lost, we revoke it and issue another.

Your IP address

The key is not the only gate. This API answers only requests from the IP addresses you registered with us — usually the static outbound address of the server that will be calling. From anywhere else the connection is refused before the key is even read, so a leaked key is not usable from a stranger's machine.

  • Send us the address (or CIDR range) your calls will come from, before you start.
  • Tell us before you move providers, add a second server, or change your firewall — otherwise your integration stops the moment the address changes.
  • If your outbound address is dynamic, say so and we will work something else out.
Use the HTTPS endpoint. $BASE is https://api.richview.com. The older http://a.richview.com:8101 address is plain HTTP — your key travels in clear on it — and is being retired; move any integration still pointing at it. The portal you sign into in a browser is a separate hostname, https://trade.richview.com, and it does not accept API keys at all: keys work only from the address you registered, over HTTPS.

Everything needs a credential

Not just pricing. The product list, fabrics, options and the price book all require your key too — there are no anonymous endpoints here beyond a health check. If you get a 401 from a plain GET, that is why.

Your account is never something you send. It is resolved from your key. There is no account, discount or price field on any request, so a bug in your code cannot accidentally price as someone else.

Your discount

GET/dealer/me key or session

Your account, company, and multiplier for every product you are set up for. Useful at start-up to check your discount, or to show it in your own UI.

{
  "account": "XX-000",
  "company": "YOUR COMPANY LTD",
  "discounts": [
    { "blind_type_code": "P60", "multiplier": "0.250", "percent_off": "75.0" },
    { "blind_type_code": "RC",  "multiplier": "0.400", "percent_off": "60.0" }
  ]
}
Your discount is per product, not one rate across the board — so do not cache a single multiplier and apply it everywhere. Always price through the API.

This lists only the products in your contracted range. If you hold terms with us on something not sold through this API, it will not appear here.

Price a blind

POST/dealer/quote key or session
FieldTypeNotes
blind_type_codestringe.g. P60. See Products.
fabric_codestringMust belong to that product. See Fabrics.
dimensions.width_mmstringMillimetres. inches × 25.4
dimensions.height_mmstringMillimetres.
optionsarray{name, value} pairs. See Options.
quantityint1–100. Default 1.
multi_blindintShades on one headrail, 1–8. Default 1. See Sizing.

Price many at once

POST/dealer/quote/batch key or session

Send an array of the same line objects, up to 50. Use this for a whole job rather than one request per window.

Results come back in order. A bad line returns an error entry instead of failing the batch, so four windows with one typo still price the other three.

[
  { "index": 0, "quote": { "unit_price": { "amount": "100.80", … } } },
  { "index": 1, "error": { "field": "fabric_code",
                           "message": "Fabric 'XYZ' is not available for Economy Cellular Shade.",
                           "allowed": ["PS31G0001", "PS31G0004", …] } },
  { "index": 2, "quote": { … } }
]

Products

GET/storefront/products key or session

Every product you can order, with its lead time and starting price. Note the starting price here is list — your net comes from /dealer/quote.

[ { "code": "P60", "name": "Economy Cellular Shade",
    "from_price": "116.00", "currency": "USD",
    "fabric_count": 56, "lead_time_days": 6 } ]
This list is your range, not our whole catalogue. It returns exactly the products your account is set up for, so you can drive your own product picker straight off it. A code outside the range is not hidden-but-orderable — it returns 404 from every endpoint here, including the price book. To add a product to your range, talk to your account manager; it is a change at our end, not a parameter you can send.

Fabrics

GET/products/{code}/fabrics key or session

Paged and searchable — some products carry 400+ fabrics, so fetch what you need rather than the lot.

Each fabric carries a type — the cloth itself, shared by every colour in the same price group, e.g. 3/4 LF SINGLE — alongside its name, which is the colour. Group by type and you get the same two-step choice as our order entry: fabric type first, then colour. A handful of older fabrics have no type recorded; those come back with type empty.

{ "code": "PS410001", "name": "Cotton 001", "type": "3/4 LF SINGLE",
  "light": "light_filtering", "price_group": "pls410" }
QueryNotes
qSearch name or code.
lightlight_filtering, room_darkening, blackout, sheer
offset, limitPaging. Limit 1–500, default 60.
GET /products/P60/fabrics?light=blackout&limit=20

Options

GET/products/{code}/config?include_fabrics=false key or session

The valid option names and values for a product, with a price hint where the option costs extra. Do not hard-code these — they change with the price book.

P60 today

OptionValuesAdds
ControlTypeCORDED, CLUTCH, CORDLESS
MOTORIZED$540
StackBOTTOM_UP
TOP_DOWN_BOTTOM_UP$80
RemoteChannelNONE / ST15$130
TileCutOutNONE / ONE_SIDE / TWO_SIDES$63 per side
MeasureNameINSIDE_MOUNT / OUTSIDE_MOUNT
Option charges are added to the size price, then your multiplier applies to the total — the same way it appears on your invoices.

Optional hardware

GET/accessories key or session

Battery packs, solar panels, cords and the P-BOX hub. These are separate line items, not shade options — per: "order" means one per order (the hub), per: "shade" means one per blind.

The prices here are list, and your shade discount is not applied to them. If you have agreed a rate on hardware, confirm it with your account manager before ordering.

How sizing works

Prices come from a width × drop grid. A size rounds up into the next published band — the same rule your invoices are priced on — so 35"×47" and 36"×48" can cost the same, while 36.5" moves to the next band.

Send millimetres. We match at whole-millimetre precision, so a plain 36" (914.4 mm against a band published at 914 mm) lands in the band you would expect rather than the next one up.

Sizes beyond the largest published band are rejected, never extrapolated. Check the range in /products/{code}/config before sending.

Multi-headrail

For a 2-on-1 or 3-on-1, send the total headrail width and set multi_blind. The price is for the whole headrail; the response adds per_shade_price showing the split.

{ "…": "…", "multi_blind": 2 }
→ "unit_price": { "amount": "370.00" },
  "per_shade_price": { "amount": "185.00" }

What this API does not do

Worth knowing before you design around it, so you do not build against something that isn't here.

  • It does not place orders. A quote is a price, not a commitment on either side. Keep ordering on your existing channel; when order submission is available we will tell you, and it will be additive — nothing here changes.
  • It does not know stock or your delivery date. The lead_time_days on a product is the standard published lead time, not a promise for your job.
  • It does not discount hardware. Accessories come back at list.
  • It has no test or sandbox environment. Every call is against live prices. Nothing here charges you or creates a record, so that is safe — but the numbers you see are the real ones.
Prices change. Every response carries rate_card_version. If it differs between the quote you showed a customer and the order you place, re-price first — that is the field to watch, not a date.

Errors

Every rejection names the field and, where useful, lists what is allowed — so your system can show a real message rather than "invalid request".

StatusMeaningWhat to do
401Missing, invalid, or revoked keyCheck the header; create a new key if revoked.
403Signed in but not linked to a trade accountLink in the portal, or contact your account manager.
404Unknown product, or one outside your rangeCheck /storefront/products — it lists exactly what you may order.
422Bad fabric, size, option, or quantityRead detail.allowed.
429Too many requestsBack off; honour Retry-After.
{ "detail": {
    "field": "options.ControlType",
    "message": "'MOTORIZATION' is not a valid ControlType for Economy Cellular Shade.",
    "allowed": ["CLUTCH", "CORDED", "CORDLESS", "MOTORIZED"] } }
We reject an unrecognised option rather than quietly ignoring it. A typo gives you an error, never a quote that silently omits the motor you asked for.

No response at all — connection refused, or a timeout with no HTTP status — usually means the request came from an address we do not have registered. Check your server's outbound IP against what you told us; it is not something the API can tell you, because the connection never gets that far.

Rate limits & good manners

  • 300 quotes per minute. Use /dealer/quote/batch for jobs — one request of 50 lines beats 50 requests.
  • Cache the catalog, not prices. Products, fabrics and options change rarely; prices you should always fetch.
  • Watch rate_card_version. If it changes between your quote and your order, re-price before committing.
  • On 429, honour Retry-After and back off.

Try it

Runs a real request against this server with your key, so you can see the shape of the response before writing any code.

Typing your key here is fine: it goes to this server and nowhere else, and nothing stores it. That is different from shipping a key inside a page your customers load, which is what Authentication warns against.
Your API key (never stored or sent anywhere else)
Product
Fabric
Width (in)
Height (in)

Prefer a schema? The machine-readable OpenAPI document is at /openapi.json — fetch it with your key, the same as any other endpoint:

curl -H "X-API-Key: $HTW_API_KEY" "$BASE/openapi.json" > htw-openapi.json

Support

Talk to your account manager for anything commercial — your discount, adding a product to your range, registering another IP address, or a key that needs replacing.

For anything technical, include these three things and we can usually answer in one round trip:

  • the key prefix you were using (htw_live_abc123 — the first few characters only, never the whole key);
  • the request body you sent and the status and body you got back;
  • the rate_card_version from the response, if you got one.

Never send us a whole API key, in an email or anywhere else. If you think one has been exposed, say so and we will revoke it — that costs you one integration restart, which is cheaper than the alternative.