Create orders
You can place orders in two ways. The embedded interface has a ready checkout. The API lets you build your own New order page and create orders from it. This page covers the API, so you can skip it completely if you use the embedded interface. Read it if you are curious. A supplier is a brand or distributor.
Show “Powered by Beauty Router” and a link to Beauty Router’s terms on your New order page. See Beauty Router mark.
A New order page makes these calls:
- Get offers for the products on the page.
- Get a quote with all the money and all the problems.
- List the payment cards.
- Place the orders.
Your page can also save a draft and place it later.
Every endpoint on this page takes the user token, so your page can call them directly. Reads work for any logged-in
user of the company. Placing orders and saving drafts need canOrder. A quote with a
deliveryAddress needs canOrder or
canManageCompany.
Wholesale prices reach the API only for companies that are prequalified or verified. For any other company, the
offers, quote, and order calls fail with company_not_verified. See
Verification statuses.
Get offers
Section titled “Get offers”POST /v1/products/offers returns what a New order page shows about each product:
| On your page | Field |
|---|---|
| Price | unitPrice on the offer |
| Quantity discounts | discountLevels on the offer |
| Order discounts | orderDiscountLevels on the supplier |
| Stock | available, status, and plannedAvailability on the offer |
| Maximum per order | maxQuantityPerOrder on the supplier |
| Unavailable suppliers and why | status and canShipToAddress on the supplier |
| Shipping terms | shippingType and shippingFee on the supplier |
| Free shipping | freeShippingThreshold on the supplier |
| Rating | rating on the supplier |
| Recommended supplier | recommended on the offer |
It is the one batch endpoint in the API, made for the rows of a New order page. Load rows as they appear: call it for the
visible rows and the selected rows, and split a longer list into chunks of 500. It has a limit per user token, and a
request over it fails with rate_limit_exceeded.
| Field | Description |
|---|---|
productIds |
The products, up to 500. Required. |
supplierId |
Return only the offers of this supplier. Optional. |
deliveryAddress |
The delivery address, with the same fields as the company’s. Optional. |
Without deliveryAddress, Beauty Router uses the company’s delivery address. It skips suppliers that cannot ship to the
address. When you name a supplierId, that supplier comes back with canShipToAddress: false instead of being skipped,
so you can tell the user why.
A supplier that is archived_by_supplier or blocked still comes back, flagged by status, so your page can show why.
It is never recommended.
curl https://api.beautyrouter.com/v1/products/offers \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "productIds": ["prod-4Fz9QbX2kL"] }'{ "products": [ { "productId": "prod-4Fz9QbX2kL", "hazardousMaterials": false, "type": "professional", "offers": [ { "supplierId": "sup-1Ry6tUi3Op", "unitPrice": 24.50, "available": 120, "status": "published", "plannedAvailability": null, "discountLevels": [ { "minQuantity": 12, "discountPercent": 10, "discountAmount": 0.00, "name": "Salon Stock-Up", "endsAt": "2026-12-31" } ], "recommended": true }, { "supplierId": "sup-8Wq2eRt5Yu", "unitPrice": 25.25, "available": 0, "status": "unavailable", "plannedAvailability": "2026-11-12", "discountLevels": [], "recommended": false } ] } ], "suppliers": [ { "id": "sup-1Ry6tUi3Op", "name": "Amberlin", "logo": "https://images.beautyrouter.com/sup-1Ry6tUi3Op/logo.png", "rating": 94, "status": "active", "maxQuantityPerOrder": 48, "freeShippingThreshold": 150.00, "shippingFee": 12.00, "shippingType": "flat", "orderDiscountLevels": [ { "minAmount": 500.00, "discountPercent": 5, "discountAmount": 0.00, "name": "Holiday Order Bonus", "endsAt": "2026-12-31" } ], "canShipToAddress": true }, { "id": "sup-8Wq2eRt5Yu", "name": "Dunmore Beauty Supply", "logo": "https://images.beautyrouter.com/sup-8Wq2eRt5Yu/logo.png", "rating": null, "status": "active", "maxQuantityPerOrder": 100, "freeShippingThreshold": null, "shippingFee": null, "shippingType": "carrier", "orderDiscountLevels": [], "canShipToAddress": true } ], "currency": "usd"}Each product’s offers are sorted best first: in stock first, then suppliers the company is connected to, then by
rating. The first offer has recommended: true, so your page can pick a supplier for each row on its own.
| Product field | Description |
|---|---|
productId |
The product ID. |
hazardousMaterials |
Whether the product is hazardous, which limits where it can go. |
type |
professional or retail, as in the catalog. |
offers |
The offers, best first. |
| Offer field | Description |
|---|---|
supplierId |
The supplier. |
unitPrice |
The wholesale price of one unit, before discounts. |
available |
How many units can be ordered now. |
status |
published, unavailable for now, or discontinued. |
plannedAvailability |
The date the product is back in stock, or null. |
discountLevels |
Quantity discounts. See the level fields below. |
recommended |
true on the first offer. |
| Supplier field | Description |
|---|---|
id |
The supplier ID. |
name |
The supplier name. |
logo |
The URL of the supplier’s logo, or null. |
rating |
The rating from 0 to 100, or null. |
status |
See Supplier statuses. |
maxQuantityPerOrder |
The most units of one product in one order. |
freeShippingThreshold |
Orders at or above this amount ship free. null when the supplier has no free shipping. |
shippingFee |
The flat shipping charge. null when shippingType is carrier. |
shippingType |
flat for a flat amount, or carrier for a carrier quote. |
orderDiscountLevels |
Discounts on the whole order. See the level fields below. |
canShipToAddress |
Whether the supplier ships to the address. It can be false only for a supplier you named. |
| Level field | Description |
|---|---|
minQuantity |
The quantity from which the level applies. Only in discountLevels. |
minAmount |
The order amount from which the level applies. Only in orderDiscountLevels. |
discountPercent |
The discount in percent, or 0 when discountAmount applies. |
discountAmount |
The discount per unit in dollars, or 0 when discountPercent applies. |
name |
The name of the discount, or null. |
endsAt |
The date the discount ends, or null. |
Get a quote
Section titled “Get a quote”POST /v1/orders/quote is a dry run of order creation. It returns all the money and all the problems, and it creates no
order.
Call it on page load, when the address or a supplier changes, and before placing the orders. When the user edits a
quantity, estimate the line totals from the offers and keep the last tax. Quote has a limit per user token, and a request
over it fails with rate_limit_exceeded.
| Field | Description |
|---|---|
lines |
The lines: productId, quantity, and optional supplierId. Required. |
deliveryAddress |
The delivery address. Optional. Without it, Beauty Router uses the company’s. |
paymentCardId |
The card that will pay, so card problems judge that card. Optional. Without it, the default card. |
A line with no supplierId goes to the first offer, as in Get offers.
curl https://api.beautyrouter.com/v1/orders/quote \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "lines": [ { "productId": "prod-4Fz9QbX2kL", "quantity": 6 }, { "productId": "prod-6Gh2jKl8Zx", "quantity": 4 }, { "productId": "prod-2Df7gHj4Kl", "supplierId": "sup-8Wq2eRt5Yu", "quantity": 12 } ] }'{ "groups": [ { "supplierId": "sup-1Ry6tUi3Op", "lines": [ { "productId": "prod-4Fz9QbX2kL", "quantity": 6, "unitPrice": 24.50, "discount": 0.00, "total": 147.00 }, { "productId": "prod-6Gh2jKl8Zx", "quantity": 4, "unitPrice": 26.50, "discount": 0.00, "total": 106.00 } ], "productsTotal": 253.00, "orderDiscount": 0.00, "shipping": { "amount": 0.00, "kind": "free" }, "freeShippingRemaining": null, "salesTax": 22.01, "taxExemption": { "certificateId": null, "notAppliedReason": "no_certificate" }, "total": 275.01, "chargeTiming": "now", "problems": [] }, { "supplierId": "sup-8Wq2eRt5Yu", "lines": [ { "productId": "prod-2Df7gHj4Kl", "quantity": 12, "unitPrice": 9.75, "discount": 11.70, "total": 105.30 } ], "productsTotal": 105.30, "orderDiscount": 0.00, "shipping": { "amount": 14.35, "kind": "carrier" }, "freeShippingRemaining": null, "salesTax": 9.16, "taxExemption": { "certificateId": null, "notAppliedReason": "no_certificate" }, "total": 128.81, "chargeTiming": "now", "problems": [ { "code": "insufficient_stock", "message": "Only 8 units are available.", "productId": "prod-2Df7gHj4Kl", "available": 8 } ] } ], "unresolvedLines": [], "companyProblems": [], "total": 403.82, "currency": "usd"}There is one group per supplier, and each group becomes one order.
| Group field | Description |
|---|---|
supplierId |
The supplier. |
lines |
The lines: productId, quantity, unitPrice, discount on the whole line, and total. |
productsTotal |
The total of the lines. |
orderDiscount |
The discount on the whole order. |
shipping |
The shipping charge: amount, or null, and kind, which is free, flat, or carrier. |
freeShippingRemaining |
How much more to order for free shipping, or null when it does not apply. |
salesTax |
The sales tax, or null. |
taxExemption |
certificateId of the certificate used, or null, and notAppliedReason. See below. |
total |
The amount the salon pays for this group, or null. Send it back as expectedTotal. |
chargeTiming |
now, or after_verification when the company is prequalified. |
problems |
What stops this group. See Problems. |
notAppliedReason is null when a certificate applies. Otherwise it is no_certificate, other_state when the
company’s certificates are for other states, or not_reviewed when Beauty Router has not reviewed the certificate yet.
unresolvedLines lists the lines no supplier can take, each with productId, quantity, and problems.
companyProblems lists what stops every group. total is the sum of all the groups, or null when a group’s total is.
An amount Beauty Router cannot work out is null, never 0, so do not show it as free. Do not place a group with problems: fix them first, or leave the group out.
Problems
Section titled “Problems”A problem has a code and a message. Some also have productId or supplierId. Show the message, or use the code to
build your own. Fix each problem, or remove the group, before you place the orders.
| Code | Meaning |
|---|---|
buying_suspended |
Beauty Router suspended the company’s buying. |
supplier_unavailable |
The supplier cannot take orders right now. |
address_state_without_license |
The delivery address is in a state where the company has no license. |
address_not_deliverable |
A carrier cannot deliver to the address, or collect from the supplier. |
ship_to_restricted |
The supplier does not ship to the delivery address. |
hazmat_destination |
A hazardous product cannot go to the delivery address. |
connection_required |
The supplier must connect with the company before it can order. |
connection_blocked |
The supplier does not take orders from this company. |
insufficient_stock |
There is not enough stock. available says how many units can be ordered. |
product_unavailable |
The product is unavailable or discontinued. |
shipping_unavailable |
Shipping cannot be calculated for this order. |
tax_unavailable |
The sales tax cannot be calculated for the delivery address. |
delivery_address_required |
The company has no delivery address. |
delivery_phone_required |
Neither the delivery address nor the company has a phone number. |
payment_card_required |
The company has no payment card. |
payment_card_unusable |
The payment card expired or cannot be used. |
quantity_limit_exceeded |
A line is over the supplier’s maxQuantityPerOrder. |
order_limit_exceeded |
The order is over the maximum amount for one order. |
order_below_minimum |
The order is below the minimum order amount. |
Choose a payment card
Section titled “Choose a payment card”GET /v1/company/payment-cards lists the company’s cards, so your page can show which one pays. See
Payment card. When the company has no usable card, open showPaymentCards(element) so the
user can add one.
Place the orders
Section titled “Place the orders”POST /v1/orders/batch creates one order per group. It takes a required Idempotency-Key header.
| Field | Description |
|---|---|
orders |
The orders: supplierId, lines, expectedTotal, and optional draftOrderId. |
deliveryAddress |
The delivery address. Optional. Without it, Beauty Router uses the company’s. |
paymentCardId |
The card that pays, such as card-6Mn3bVc8Xz. Optional. Without it, the default card pays. |
saveAsDraft |
true saves the whole request as drafts. Optional, default false. |
Each line has productId and quantity. expectedTotal is the group’s total from the quote.
curl https://api.beautyrouter.com/v1/orders/batch \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 4f0c2b7e-9a1d-4c36-b8e5-2d7a6f1e3c90" \ -d '{ "orders": [ { "supplierId": "sup-1Ry6tUi3Op", "lines": [ { "productId": "prod-4Fz9QbX2kL", "quantity": 6 }, { "productId": "prod-6Gh2jKl8Zx", "quantity": 4 } ], "expectedTotal": 275.01 } ], "paymentCardId": "card-6Mn3bVc8Xz" }'The response is 201, with the new orders in data. Each one has the same fields as any other
order. Your server also receives an order.created webhook for each one.
{ "data": [ { "id": "order-8Jd3kFs6Gh", "number": "4821-7730", "status": "placed", "total": 275.01, "currency": "usd" } ]}The example shows only some of the fields.
Idempotency key
Section titled “Idempotency key”Send a new Idempotency-Key for each order the user places, such as a UUID. When a request times out, send it again
with the same key and the same body. Beauty Router returns the first result and does not create the orders twice. A refused request creates nothing and stores no key, so you can repeat it with the same key.
A key belongs to the company and lasts 24 hours. The same key with a different body fails with 409
idempotency_key_reused. While an earlier request with that key is still finishing, a repeat fails with 409
idempotency_key_in_use, so try again shortly.
When the total changed
Section titled “When the total changed”Beauty Router recalculates each group, shipping included. When a group differs from expectedTotal by $0.10 or more, the
request fails with 409 order_total_changed, and nothing is created. The error has a fresh quote in quote. Show the
new amounts to the user, then send the request again with the new totals.
A quote is not an object and holds no price. Prices can change until the orders are placed, and expectedTotal is how
you find out.
When a group has problems
Section titled “When a group has problems”Beauty Router checks every group before it creates anything. When any group has a problem, or
companyProblems lists one, the request
fails with 409 order_has_problems, and nothing is created. The error has a fresh quote in quote, which lists the
problems per group.
Payment
Section titled “Payment”The orders are created together, and Beauty Router charges the card at once. The response has each order after its charge
attempt, with its final status: placed, payment_failed, or pending_verification for a prequalified company. A failed
charge never fails the request: that order comes back as payment_failed, and the other orders stand.
The charge runs while the user is not present, so a card that needs 3-D Secure fails. When an order is payment_failed,
open showOrder(element, orderId), where the user pays with 3-D Secure or chooses another card.
After the order
Section titled “After the order”Shipping is locked when the order is created. Until the order is placed, other amounts can still change, so keep the
order up to date from order.updated. See Order fields.
Drafts
Section titled “Drafts”A draft saves an order for later. Send saveAsDraft: true to POST /v1/orders/batch. There is one draft per supplier,
like orders. Idempotency-Key stays required, and expectedTotal is not needed.
curl https://api.beautyrouter.com/v1/orders/batch \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 7b3e1d94-2c6a-4f08-a1d7-5e9c0b8f4a26" \ -d '{ "saveAsDraft": true, "orders": [ { "supplierId": "sup-1Ry6tUi3Op", "lines": [{ "productId": "prod-4Fz9QbX2kL", "quantity": 6 }] } ] }'The response is 201, with the drafts in data. Each one has the status draft.
A draft is not an order yet:
- It holds no stock.
- The supplier never sees it.
- It sends no webhook, no email, and no notification.
order.createdis sent when the draft is placed.
The company needs the same status as for placing an order, prequalified or verified. Otherwise the request fails with
company_not_verified.
A draft can have no address and no card. With no address, salesTax, shipping, and total are null, never 0.
Find your drafts
Section titled “Find your drafts”GET /v1/orders lists drafts with the other orders, status=draft returns only drafts, and GET /v1/orders/{orderId}
returns one. See List orders.
Update or place a draft
Section titled “Update or place a draft”Send the draft’s ID as draftOrderId on the order. With saveAsDraft: true, the request updates the draft. With
saveAsDraft: false, it places the draft, and the order needs expectedTotal as usual. Either way the ID and the number
stay, and the lines are replaced. The supplier cannot change.
curl https://api.beautyrouter.com/v1/orders/batch \ -H "Authorization: Bearer $USER_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: c81f6a30-94de-4b57-8e12-3a7d05f6b9c4" \ -d '{ "orders": [ { "draftOrderId": "order-2Xc9vBn4Mq", "supplierId": "sup-1Ry6tUi3Op", "lines": [{ "productId": "prod-4Fz9QbX2kL", "quantity": 6 }], "expectedTotal": 171.79 } ], "paymentCardId": "card-6Mn3bVc8Xz" }'Values are not merged. An address, a phone number, or a card you leave out falls back to the company’s default, not to the draft’s value.
Saved amounts are not refreshed. On a reopened draft, get offers and a quote before you place it. Beauty Router compares against current prices when the draft is placed.
Problems on a draft
Section titled “Problems on a draft”Saving a draft checks only these problems:
product_unavailableandorder_below_minimum.hazmat_destination,shipping_unavailable, andtax_unavailable, when the address is known.supplier_unavailable, when the supplier is not active or has no drop-off point.address_not_deliverable, for the supplier’s drop-off point.
Every other code is checked only when the draft is placed, such as buying_suspended, insufficient_stock, and the
address and card codes. A company whose buying is suspended can still save a draft.
Delete a draft
Section titled “Delete a draft”DELETE /v1/orders/{orderId} deletes a draft. See Delete a draft.
What stays embedded
Section titled “What stays embedded”Some steps stay in Beauty Router’s interface, because they handle card details or need the user present:
| Step | Function |
|---|---|
| Entering a payment card | showVerification(element), showPaymentCards(element) |
| 3-D Secure | showOrder(element, orderId) |
| Paying again after a failed charge | showOrder(element, orderId) |
| Choosing another card on a failed order | showOrder(element, orderId) |
