---
title: "How do you price one product differently per region, segment or contract?"
blueprint: 007
slug: pricing-one-product-many-prices
category: Pricing
revised: 2026-09-03
platform: Coevera CRM (formerly Pipeliner CRM)
canonical: https://blueprints.coevera.com/blueprints/pricing-one-product-many-prices/
publisher: Coevera
customer_data: none
---

# How do you price one product differently per region, segment or contract?

**Short answer.** **A product carries no price.** The price lives on a join record between the
product and a **price list**, holding the amount, its currency, and an **access setting** that can
restrict the list to particular roles. Two lists, two rows, one product, two prices.

Two things will catch you. Creating a price list **does not** create price rows for products that
already exist — only the reverse. And the **API never resolves a price from a list**: a line item
created without an explicit price gets zero, even when the product is priced on the default list.

## 01 · The business problem

The same product does not cost the same to everyone. A distributor buys at one price, a direct
customer at another. A framework contract fixes a price for a year. A region has its own list
because the market bears something different. A partner tier gets a discount structure the direct
sales team should not be able to see, let alone quote.

What that means in practice:

- one catalogue entry per product — not four near-duplicates with different prices, which is how
  this goes wrong;
- several prices per product, each belonging to a named list;
- the right price applied when a salesperson builds a quote, without them having to look it up;
- some lists visible only to some people;
- and a record, afterwards, of what was quoted and on what basis.

## 02 · Why the obvious approach fails

### Duplicating the product per price

"Widget (EMEA)", "Widget (Partner)", "Widget (Contract A)". It works on day one and decays
immediately: four records to keep in step whenever a specification changes, four SKUs where the
business has one, and reporting that cannot answer "how much Widget did we sell?" without a
mapping table somebody maintains by hand.

### Putting the price on the product as a custom field

A custom field per tier — *list price*, *partner price*, *contract price* — puts the whole pricing
matrix on one form, visible to everyone who can see the product, and requires a schema change
every time a tier is added. It also bypasses the line-item mechanics entirely, so nothing carries
the price onto a quote for you.

### Assuming the product's price is a property of the product

> **It is not, and this is the structural fact worth internalising.** Reading the Product entity's
> schema, `price` appears in its list of key fields — but there is no price attribute on the
> entity. Create a product and the record comes back with a name, a SKU, a unit symbol, a type,
> and *no price anywhere*.
>
> The price is a separate record: a join between the product and a price list. Every surprise in
> §6 follows from that one fact.

## 03 · Data model

Three entities, one of which people do not know exists.

| Entity | Holds |
|---|---|
| **Product** | The catalogue entry — name, SKU, unit, type, category. No price. |
| **Price list** | A named list — "List Price", "EMEA", "Partner Tier". Carries a type and an *active* flag. |
| **Price-list price** *(the join)* | **The price itself**, plus its currency, plus an access setting. One row per product × list. |

So "this product costs 100 on the default list and 85 on the EMEA list" is two join records, not
two fields and not two products.

> **The join carries access control.** Each price row has an access setting with an optional role
> list. That is the mechanism for a partner or internal-only tier: the restriction lives on the
> price, not on the product and not on the list's name. It is also easy to miss entirely, because
> nothing about a price list's name suggests that permissions are attached a level below it.

### What a fresh space starts with

- **One price list**, named "List Price", flagged delete-protected — you cannot remove it.
- That list arrives with its **active flag set to false**, which surprises people who assume the
  default list is ready to use.
- **One currency**, the base currency, delete-protected.

### Line items

A product reaches a deal as a line item — a join between the opportunity and the product, carrying
**price**, **quantity** and a computed **amount**. Line-item pricing exists on **both Quote and
Opportunity**; an opportunity is not restricted to a single summary figure.

The opportunity additionally carries its own **product currency**, separate from the currency of
its headline value, and a flag for whether that headline value is derived from the line items or
entered by hand.

## 04 · Setting it up

1. **Create the price lists first**Before the catalogue, if you can. The order matters — see §6.
2. **Activate them**Including the built-in default, which does not arrive active.
3. **Create the products**Each one automatically gets a price row for every list that already
   exists, defaulting to zero.
4. **Set the prices**One row per product per list. This is the bulk of the work and the part worth
   scripting.
5. **Set access on the rows that need restricting**Not on the list — on the price rows within it.
6. **Verify by fill rate**Count price rows per list and compare against the product count. A list
   with fewer rows than products has gaps, and a gap reads as a price of zero.

Step 6 exists because of the asymmetry in §6, and it is the check that catches the most common way
this configuration is silently incomplete.

## 05 · Applying the right price

> **The API does not resolve a price from a price list.** Verified: a line item created without a
> price came back with `price: 0`, on a product that had a price of 100 on the default list. No
> error, no default, no lookup.
>
> Price-list selection is an *interface* affordance. On the API path, deciding which list applies
> and reading the price out of it is the caller's job.

For an integration or an import, that means the resolution logic is yours to write and yours to
keep correct: determine the applicable list from whatever drives it — the account's region, its
tier, a contract reference — read the price row for that product and list, and write the value
onto the line item.

**And the line item does not record which list the price came from.** There is no price-list
reference on it. So once a line exists, nothing in the data says whether 85 was the EMEA price, a
negotiated discount, or a typing error. If the basis of a price matters for reporting or audit,
capture it yourself in a custom field on the line item at the moment you set the price —
afterwards is too late.

## 06 · Limits & trade-offs

### Price-row creation is one-directional

> **Creating a product creates a price row for every existing list. Creating a list creates
> nothing for existing products.** Verified in both directions.
>
> So the natural sequence — build the catalogue, then add a regional list when the business
> expands — leaves **every existing product unpriced on the new list**, and an unpriced row is
> indistinguishable from a price of zero. Add lists before products where you can; where you
> cannot, backfill the rows deliberately and verify the count.

### Computed values lag the write response

A line item's `amount` is computed from price × quantity, but the value in the response to your
own write is stale. Immediately after setting price 85 on a quantity of 2, the write returned
`amount: 0`; a fresh read returned `170`. The calculation is correct — the echo is not.

**Never assert a computed field from a write response.** This nearly produced a wrong claim in
this very blueprint; the same pattern appears in [Blueprint
002](https://blueprints.coevera.com/blueprints/ai-fields-read-documents/).

### Discount fields are readable but not writable

The line item exposes a discount value and a discount percentage, and the API rejects attempts to
write either — it reports them as attributes the entity does not have. They appear to be derived
rather than settable, so a discount has to be expressed by adjusting the price rather than by
recording a discount.

### Unverified: the opportunity value rollup

With the opportunity's auto-calculate flag set to true and a line item whose amount computed
correctly, the opportunity's headline value stayed at zero across repeated reads and a deliberate
touch of the record. We then repeated the test with the flag set *at creation*, before any line
item existed — same result: the line item's amount computed to 1,000 and the opportunity value
remained zero.

**We are still not calling it a defect**, because the obvious remaining explanation is that the
rollup is computed by the interface rather than the server. But both orderings are now ruled out,
so **an integration cannot rely on line items to drive opportunity value** — set the value
explicitly if you need it, and confirm the interface behaviour before assuming otherwise.

### Multi-currency

Each price row carries its own currency, so a list can be denominated in a different currency from
another. What is *not* native is retaining the exchange rate that applied at a point in time — an
exchange-rate-at-transaction history has to be driven by automation if the business needs it. We
have not verified this in a multi-currency space; it is recorded from prior analysis rather than
observation.

## 07 · Verification

- **Count price rows per list against the product count.** The single most valuable check here,
  because an unpriced product on a list looks exactly like a product priced at zero.
- **Read prices back from the join records**, not from the product — the product has nothing to
  read.
- **Create one line item end to end** and confirm the price that lands is the one you intended,
  especially if an integration is doing the resolving.
- **Re-read computed amounts after a settle.** Never from the write response.
- **Test access restrictions as a restricted user**, not as an administrator — the same discipline
  as the record-lock check in [Blueprint
  004](https://blueprints.coevera.com/blueprints/quote-approval-thresholds/). An administrator
  cannot see the restriction working.
- **Confirm the default list is active** before wondering why nothing prices.

**What would signal a regression:** line items landing at zero, which means the resolution step
was skipped; a price list whose row count drifts below the product count after a catalogue
addition; or quoted prices that no longer match any list, which means somebody has been editing
line items directly.

---

Published by Coevera. Abstracted to the reusable pattern — no customer names, no client
data, no personal data.
