Skip to content

For Shopify agencies

Shopify B2B quote architecture: what lives in Shopify and what belongs in the quote layer

By Jahangir Alam · September 20, 2026 · 16 min read

Last verified
Shopify API
2026-07
Audience
Shopify agencies, developers and solution architects
Scope
Native Shopify B2B objects, a quote layer (app, custom build or CPQ) and the two hand-offs between them

Keep Shopify as the system of record for everything it already has an object for - products, variants, inventory, customers, companies, locations, catalog prices, payment terms, draft orders and orders - and give the quote layer only what Shopify has no object for: the buyer's request, the seller's proposals and their versions, counter-offers, approvals, the negotiation's audit trail, and the prices at the moment they were agreed. The quote layer reads Shopify live, stores references and dated snapshots rather than copies, and hands Shopify one draft order when the price is settled. That is the whole architecture; the rest of this page is the reasoning and the mechanics, verified against Shopify's own documentation.

This is written for the agency or developer scoping a B2B build, not for the merchant choosing an app. It applies whether the quote layer is an app, a custom build or a CPQ; where a section describes how QuotWay does it, that is stated as an example of the pattern, not as the pattern.

The rule: no second commerce database

Every B2B quoting project meets the same temptation early. Quotes need product titles, prices, customer names, company details and stock levels, so the quote system starts keeping its own tables of products, prices and customers, and within a quarter the store has two catalogs, two price lists and two customer records that disagree. Shopify's native B2B model already owns each of those, and it changes them without telling anyone: a merchant edits a catalog price, a location gets a second catalog, a variant is deleted, a contact moves to another location.

The rule that avoids this is short. Shopify owns any object it has; the quote layer owns only the objects Shopify lacks. Where the quote layer must hold a value from Shopify - a title on a proposal PDF, the price a negotiation started from - it holds it as a dated snapshot with the Shopify ID beside it, so the record says "this is what the catalog said on 14 March", never "this is the price".

The native model is narrower than it looks, which is what makes the split clean. Shopify's B2B objects are Companies, company locations, company contacts, catalogs (a product selection plus a price list, quantity rules and volume pricing) and draft orders, and the location is the entity actually sold to: catalogs, payment terms, tax exemptions and checkout settings all attach to it. None of those objects can hold a request for a price, a proposal with a version number, a counter-offer, an approval, or a history of who changed what. A draft order is the record of an order awaiting payment or approval, not of the negotiation that produced it. The Shopify B2B technical reference lists the object model in full.

Which system owns what

The table is the architecture. Each row names the owner, what the quote layer is allowed to keep, and why.

Data System of record What the quote layer keeps Why
Products and variants Shopify Product and variant IDs, plus a title snapshot refreshed on each proposal version A March proposal must still read correctly in September; the ID keeps it tied to the real product
Inventory Shopify (fed by the ERP where one exists) Nothing. Read at conversion time, flag drift Stock changes hourly; a copy is wrong by the time it is written
Customer identity and login Shopify customer accounts The customer ID; for guests, an email and a claim to a customer record later Shopify owns authentication; B2B buyers sign in with a one-time code to the current customer accounts
Company, location, contact Shopify B2B The three IDs Catalogs, terms and tax exemptions hang off the location in Shopify; duplicating the company means duplicating all of them
Standing prices, quantity rules, volume pricing Shopify catalogs The price the negotiation started from, resolved for the buyer's location at proposal time, with the catalog ID as provenance Catalog prices change and the lowest of several catalogs wins; a stored copy cannot know that
Payment terms Shopify B2B A reference to the terms template chosen for the deal, applied when the draft order is created Terms belong on the order, and Shopify collects against them
Deposit Shopify (Plus) The percentage negotiated for the deal, versioned with the proposal Collected by Shopify checkout on the converted draft order
Tax Shopify An estimate on the proposal, recalculated by Shopify at conversion and checkout Tax depends on the location's exemptions and the ship-to address at the moment of checkout
Quote request Quote layer The whole thing: lines, quantities, requested prices, the buyer's message, custom fields, PO number Shopify has no request object
Proposals and versions Quote layer Every version, immutable once sent A sent proposal's lines are locked; a change is a new version or a counter, never an edit in place
Counter-offers Quote layer Each counter as its own version, with who made it The negotiation is the product; its history is the evidence
Approvals Quote layer Policies, the chain each proposal went through, every decision with its author Shopify staff permissions scope which accounts a rep can see, not whether a discount may go out
Audit trail Quote layer Every state change, actor and timestamp An agency will be asked "who approved this price" a year later
Accepted order Shopify draft order, then order The draft order and order IDs written back to the quote The order is Shopify's; the quote points at it
Fulfilment Shopify and the ERP Nothing The quote layer's job ended at the order

Two rows deserve a second look because they are where projects go wrong: pricing and identity.

Shopify B2B quote architecture Three columns. Left, Shopify owns products and variants, inventory, customers and companies, catalogs with prices, payment terms and deposits. Middle, the quote layer owns the request, proposal versions, counter-offers, approvals and the audit trail. Right, Shopify owns the draft order, the order and fulfilment, with the ERP behind them. Two arrows cross the boundary: the catalog price flows into the quote layer through contextualPricing when a proposal is written, and the negotiated price flows out as a priceOverride on the draft order line when the quote is accepted. A third arrow returns the order ID from the orders/create webhook. Shopify - system of record Products, variants, inventory Customers, customer accounts Companies, locations, contacts Catalogs: prices, quantity rules Payment terms, deposits (Plus) Tax settings, exemptions Markets, currencies Quote layer - the negotiation Request: lines, asks, PO, note Proposal versions (immutable) Counter-offers Approval chain and decisions Audit trail, messages, PDFs Snapshots: title, baseline, IDs Shopify - the order Draft order (purchasingEntity) Invoice, checkout, terms Order Fulfilment ERP / accounting (behind) catalog price in contextualPricing negotiated price out priceOverride order ID back orders/create Nothing in the middle column is copied from the left; everything on the right is created once, when the price is agreed.
The boundary and the two crossings: a catalog price comes in when a proposal is written, a negotiated price goes out when the quote is accepted.

Why the quote layer must not own pricing

The most common architecture mistake in Shopify B2B quoting is a quote app that keeps its own price list, imported from Shopify or from the ERP, "so we can negotiate from it". Three properties of Shopify catalogs make that a copy that is wrong within days.

First, a company location can have several catalogs, and when the same product appears in more than one, Shopify shows the lowest price. There is no precedence setting to replicate; the only correct answer is the one Shopify computes for that location at that moment. Second, volume pricing fixes a product's price and switches off the catalog's overall percentage adjustment for it, so the "list minus 20 percent" a copy would compute is not what the buyer sees. Third, catalogs are edited constantly - by the merchant, by an ERP sync, by Shopify itself when a market or currency changes. Off Plus, a store is limited to three active catalogs across all its B2B markets and cannot assign one directly to a location, so those three change often. The catalogs guide walks through the merchant's side of this.

The API gives the quote layer the right primitive instead of a copy. ProductVariant.contextualPricing takes a context of companyLocationId, country or locationId and returns "the final price after all adjustments are applied", together with the quantity rule and the price breaks that apply in that context. The correct pattern is:

  1. When a proposal is written, resolve each variant's price through contextualPricing for the buyer's company location, in the quote's currency.
  2. Store that number on the proposal line as the baseline the negotiation started from, with the catalog ID beside it as provenance.
  3. Negotiate from the baseline. The requested, offered and final prices are the quote layer's own data, because Shopify has no object for them.
  4. Never write the negotiated price back into the catalog. The catalog is the standing price for the location; the deal is one order.

QuotWay's company-aware quoting does exactly this: the company-location catalog price is resolved server-side when a proposal is sent and stored on the line as the baseline in the quote's currency, and the Shopify catalog ID is recorded on the version. It never imports a price list. The same rule answers the question "should the ERP push prices into the quote app?" - no; the ERP pushes prices into Shopify catalogs, and the quote layer reads them from there like every other channel does.

How the negotiated price reaches the order

This is the crossing in the other direction, and it has two details that catch teams building their first conversion.

The first is that the draft order is created for the company, not for the person. draftOrderCreate takes a purchasingEntity of the form { purchasingCompany: { companyId, companyLocationId, companyContactId } }, and Shopify's draft-order documentation says that a draft order with a B2B customer and a company location assigned "automatically reflect[s] the settings for that company" - prices, payment terms and checkout options. That sentence describes a draft created in the admin. Do not read it as "the API will price my lines from the catalog": a line item created with a variantId is priced by Shopify, and a Shopify Community thread that has been collecting the same question since September 2023 shows the standard result - a product with an $8 catalog price for the company arriving on the draft at its $10 product price.

The second detail is the field that carries the agreed number. DraftOrderLineItemInput.priceOverride is "the price override for the line item", set in the presentment currency, and it is the field a negotiated price must go through on a variant line; originalUnitPrice is deprecated, and title, taxable and requiresShipping are ignored when a variantId is present. A custom line (no variant) is the reverse: it carries its own title, price, taxable flag and shipping flag.

So the conversion path is:

  1. Build the draft order input from the accepted proposal version: purchasingEntity from the quote's company, location and contact IDs; each accepted line with its variantId, quantity and priceOverride at the final unit price; custom lines with their own price; the PO number; the payment terms chosen for the deal; the deposit percentage on Plus.
  2. Run draftOrderCalculate first and compare Shopify's subtotal, tax and shipping with the proposal's figures. Tax and shipping will legitimately differ - Shopify calculates them against the location's exemptions and the ship-to address now, not when the proposal was written - so the point is to record the drift and to catch the cases that need a person: a variant that no longer exists, a line Shopify prices differently, inventory that is no longer there.
  3. Create the draft order under an idempotency key, so a retried request or a second worker cannot produce two draft orders for one acceptance.
  4. Send the invoice with draftOrderInvoiceSend, or let the buyer pay through the checkout URL Shopify returns. A draft order becomes an order when it is paid or completed with draftOrderComplete; it does not hold stock or lock its prices unless you tell it to.
  5. Write the draft order ID back to the quote at creation, and the order ID when the orders/create webhook arrives. From here on the quote points at Shopify's record and stops being the source of anything.

QuotWay's conversion service is built this way: a preview through draftOrderCalculate with tax, shipping and inventory drift recorded against the quoted figures, an idempotency key and a concurrent-create claim before draftOrderCreate, priceOverride on variant lines and originalUnitPriceWithCurrency on custom lines, and the order ID populated from the orders/create webhook. What can go wrong between quote time and conversion time is its own subject; the draft-order limits post covers the Shopify side of it today and a dedicated failure-mode piece is planned.

Identity: customer, contact, guest

Shopify owns the buyer's login. B2B buyers sign in to the current customer accounts with the email on a company location and a one-time code; legacy customer accounts were deprecated on 26 February 2026 and never supported B2B, so a quote layer should not carry a password store, a "legacy accounts" path, or its own identity model. A buyer with several locations picks one before they see prices, and every price the storefront shows is a location's price.

What the quote layer keeps is the customer ID and, for company-aware quotes, the three company IDs. The one case where it legitimately holds more is the guest: a buyer who requests a quote before they have an account, or from a store that has not activated customer accounts. Then the quote layer needs an email and a way to reach the buyer - a signed link, a one-time code of its own - and a way to attach the quote to the Shopify customer once one exists. QuotWay routes signed-in buyers to a customer account UI extension inside their Shopify account and guests to a hosted portal, and claims the guest's quotes onto their customer record when they later sign in; the choice is made per store from what Shopify reports, not by the merchant.

Terms, deposit, PO, currency and tax

These are the fields that look like the quote layer's data and are not.

  • Payment terms belong to the location in Shopify (Net 7 to Net 90, due on fulfilment, a fixed date, or none), and the order collects against them. The quote layer stores a reference to the terms template chosen for this deal and applies it when the draft order is created; it never runs its own receivables.
  • A deposit is a Shopify Plus feature - a percentage due at checkout, the balance on the terms - set through DraftOrderInput.deposit since API 2026-07. The quote layer's job is to negotiate the percentage and version it with the proposal. QuotWay does this on Plus stores from the Professional plan, and never off Plus.
  • The PO number is captured on the request and passed to the draft order, where Shopify shows it on the order.
  • Currency is fixed when the quote is created, in the buyer's presentment currency for the market, and every price on every version is in that currency; a snapshot of the exchange rate at creation is kept for reporting, not for pricing. Shopify converts nothing on the quote layer's behalf.
  • Tax on a proposal is an estimate. Shopify calculates the real figure at draftOrderCalculate and again at checkout, against the location's tax exemption and the ship-to address, which is why the proposal should say "estimated" and the conversion step should record the difference rather than try to prevent it. The tax-exemption guide covers the merchant setup.

Where the ERP sits

Shopify's own B2B documentation treats ERP, accounting and PIM connections as part of the B2B architecture, and most agency builds have one. The placement question is simpler than it sounds once the ownership table is fixed: the ERP integrates with Shopify, not with the quote layer.

  • Inventory flows ERP to Shopify. The quote layer reads Shopify stock at conversion and never the ERP directly.
  • Standing prices flow ERP to Shopify catalogs, on whatever schedule the merchant runs. The quote layer resolves prices from the catalog through contextualPricing, so a price change in the ERP reaches a new proposal as soon as it reaches the catalog.
  • Orders flow Shopify to the ERP, through the connector the merchant already uses for D2C orders. A converted quote is an ordinary Shopify order with a PO number, a company, payment terms and possibly a deposit; the ERP sees it the way it sees every other order. Shopify's Spring '26 Edition added native QuickBooks sync of B2B orders, PO numbers and company details, which is the same shape.
  • The negotiation itself stays in the quote layer. If the ERP or a CRM needs to know a quote was sent, countered or accepted, that is an event, not a record to duplicate: Shopify Flow triggers are the native surface for it, and a quote layer that emits them lets the merchant wire the rest without a custom integration. QuotWay exposes Flow triggers from the Professional plan and Flow actions on Enterprise; it has no direct ERP or CRM connectors, and this architecture is why it does not need them to fit.

The one ERP-led variant worth naming is the merchant whose contract prices live in the ERP and change per customer per week. That merchant still syncs the prices into Shopify catalogs - because the storefront, checkout and draft orders all read from catalogs - and the quote layer reads from the same place. A quote layer that pulled prices from the ERP instead would be the second commerce database the rule exists to prevent.

What the quote layer keeps anyway, and why it is not a copy

Three things in the middle column look like duplicates of Shopify data. They are snapshots, and the distinction matters.

  • Titles and SKUs on proposal lines. A proposal PDF sent in March is a commercial document; if the variant is renamed in June, the March document must still read as it was sent. So the title is refreshed on each new version and frozen with it.
  • The baseline price. The figure the negotiation started from is part of the negotiation's history, and the catalog will have moved on. It is stored with the date and the catalog ID, and it is never used to price anything after that version.
  • The final prices. These are not Shopify data at all until the draft order exists; they are the outcome of the negotiation.

The test for whether something is a snapshot or a copy: a snapshot has a date and an ID beside it and is never read to answer "what is the price now"; a copy is read as if it were current. Every value in the quote layer should pass that test.

Uninstall, erasure and data residency

Because the quote layer owns nothing Shopify has, uninstalling it removes nothing from the store: products, customers, companies, catalogs, draft orders and orders are untouched. What the quote layer holds - requests, versions, approvals, messages, documents - follows Shopify's mandatory compliance webhooks, which every App Store app must implement: shop/redact arrives 48 hours after an uninstall and the app must erase the store's data; customers/redact arrives 10 days after a deletion request, or once six months have passed since the customer's last order; customers/data_request asks for the data the app holds on one customer. An agency evaluating any quote layer should ask where those records are stored, what the retention window is, and what the reinstall behaviour is within it - the evaluation checklist has those questions with the way to verify each. QuotWay's answers are in its data retention and uninstall and GDPR docs.

FAQ

Does this architecture need Shopify Plus?

No. Since 2 April 2026, Companies, locations, catalogs, quantity rules, volume pricing, payment terms and vaulted cards are on Basic, Grow and Advanced as well as Plus. Plus adds unlimited catalogs, direct catalog assignment to a company or location, deposits and partial payments. A quote layer that reads catalog prices through contextualPricing and creates draft orders with a purchasingEntity works on every plan; only the deposit row changes. The plan matrix has every difference.

Should we use draft orders as our quote object?

Use them for what they are: the record of an agreed order awaiting payment or approval. A draft order has no request, proposal, version or counter-offer state, and its prices and stock are not held unless you lock and reserve them. If the merchant's deals have one round - the buyer submits, the merchant adjusts, the buyer pays - Shopify's "submit for approval" checkout setting creates the draft order natively and a quote layer is unnecessary (when you shouldn't use a quote app lists the other cases). Once there is negotiation, approval or a history to keep, model that in a quote layer and create the draft order at the end.

What is a purchasing entity?

The B2B buyer a draft order or order is for. On draftOrderCreate it is passed as purchasingEntity: { purchasingCompany: { companyId, companyLocationId, companyContactId } } - the company, the location being sold to, and the contact placing the order. It is what makes the draft order a B2B draft order: the location's payment terms, tax exemption and checkout settings apply to it, and the buyer sees it in their company account.

Will a quote layer conflict with catalogs?

Not if it never stores a price list. The conflict arises when an app keeps its own prices and they diverge from the catalog the storefront shows. Read the location's price through contextualPricing at proposal time, negotiate from it, and write the agreed price onto the draft order line as a priceOverride. The catalog is untouched and the order matches the quote. What price an RFQ should start from when a location has several catalogs is covered in the catalogs guide.

Can a buyer edit a quotation after it has been sent?

Not the sent one. A proposal is a numbered version, immutable once it goes out, because the document the buyer received is a commercial record. The buyer's move is a counter-offer, which becomes a new version; the seller's move is a new proposal. Editing a sent version in place would make the audit trail unable to say what either side had agreed to.

Do quotes expire automatically, and does the draft order?

A quote layer can expire a proposal on a date and stop the accept link; that is the quote layer's state. A draft order has its own clock: it does not hold stock or lock prices unless you ask, and Shopify deletes draft orders after a period of inactivity - see the draft-order limits post. Create the draft order when the price is agreed, not when the quote is sent.

What happens to our quotes if we uninstall the quote app?

Nothing happens to Shopify's data. The app's own data - requests, proposals, approvals, messages - is erased when Shopify sends shop/redact, 48 hours after uninstall, subject to whatever recovery window the app documents. Export what you need before that.

Sources

Shopify pages, all read on 20 September 2026:

Related articles

See how QuotWay handles this on your store.

We’d like to set analytics cookies to understand how the site is used. They’re not required — declining changes nothing about how the site works, and you can change your mind any time on our privacy page.