To build a material price index from invoices, you need four things: a fixed basket of ten to twenty-five SKUs you buy every month, one base month to anchor against, a unit-normalized price per SKU, and quantity weights taken from your own spend. Then the index is one division: this month’s basket priced at this month’s unit prices, divided by the same basket priced at base-month unit prices, times 100. An index of 113.9 means your basket costs 13.9% more than it did in the base month.
That is the entire method. Everything else in this post is the detail that decides whether the number is trustworthy: which SKUs belong in the basket, how to normalize a unit of measure so you are not comparing a bag of 100 to an each, how to weight it, and how to separate a real price move from a change in what you bought.
Why bother, when BLS publishes free indexes? Because a public index answers a different question. It measures producer prices across a national basket, not what your branch charged your account. That argument is made in full in national material price index versus your local pricing. Your own index is the only one that has your part numbers, your branch, and your agreement in it.
What you are actually building
A fixed-basket index. You freeze the quantities at base-month levels and let only the prices move. That is deliberate: if you let quantities float, the index moves when you switch from copper to PEX on a job, and you lose the ability to say “prices went up” versus “we bought different stuff.”
Two consequences fall out of that choice, and you should know them going in.
Your index will drift from reality as your buying changes. A basket frozen in January is a good measure of January’s shop. By the following January it is measuring a shop you no longer run. Re-base once a year.
It measures your prices, not the market. If your index runs hotter than the published series for the same materials, that is a finding, not an error. It is exactly the finding worth having.
Step 1: pick the basket
Twenty SKUs is plenty. Ten works. Two hundred is a project you will abandon in week three.
Include a SKU only if it clears all four of these:
- You buy it at least monthly. A part you buy twice a year contributes noise, not signal.
- The part number is stable. If the counter substitutes freely on this item, the price series is really several series stacked on top of each other.
- It carries real spend. Rank your last twelve months by dollars per SKU and start at the top. In most shops the top twenty SKUs cover a large majority of repeat material spend.
- The unit of measure does not change. See step 2. This is where most homemade indexes quietly break.
Then check the basket for coverage. You want at least one line from every volatile family you touch: copper, wire and cable, steel and strut, PVC or CPVC, and equipment. If your whole basket is fittings, your index will tell you about fittings and nothing else.
One thing to exclude on purpose: cut wire and made-to-order material. It prices per foot off a moving base, it is routinely non-returnable and non-creditable, and it will dominate your index for reasons that have nothing to do with your agreement. Track wire separately, on the roll SKU, where the unit is stable.
Step 2: normalize the unit before you compare anything
This is the step that decides whether the whole exercise works. Every price in the basket has to be reduced to a price per stable unit, and the invoice does not always help you.
| Trap | What it looks like | Normalize to |
|---|---|---|
| Each versus bag | Fittings billed per each one month, per bag of 10 the next | Price per each |
| Roll versus foot | Romex on a 250 ft roll, or cut and billed per foot | Price per foot, and flag cuts separately |
| Length versus foot | PVC billed per 10 ft length or per foot | Price per foot |
| Box quantity change | Manufacturer moves from 100 to 50 per box, same catalog number | Price per each, re-check the box quantity |
| Pack versus case | Devices in a pack of 10 inside a case of 100 | Price per each |
A unit-of-measure change looks exactly like a 10x price increase, and a 10x price increase in your index will make you distrust the whole thing. Record the UOM alongside every price, not just the price. If your invoices are paper, capture the UOM column when you photograph them, because it is the field people crop out.
Record two more fields with every observation: branch and document type (delivered order, will-call ticket, job quote). Both explain gaps later, and both are free to capture now.
Step 3: set a base month
Pick a month where you have complete invoices and normal buying. Not a month with one enormous job in it. January is convenient because it lines up with published series and with your own year end.
For each SKU, your base price is the unit price you actually paid that month. If you bought the same SKU three times at three prices, use the volume-weighted average of those purchases, not the first one and not the lowest one. The lowest is a job quote you will not get again, and it makes every later month look like an increase.
Set the base index to 100. Every future month is a ratio against that.
Step 4: weight by your own spend
An unweighted average of price changes treats a $3.85 length of PVC the same as a $104.50 roll of wire. That is not your shop. Weight by base-month quantity, so each SKU influences the index in proportion to what you actually buy.
Here is a five-line basket worked end to end. These figures are illustrative, not quoted from any distributor’s price file. The point is the arithmetic, not the values.
| SKU | Unit | Base qty/mo | Base price | Current price | Base spend | Current spend |
|---|---|---|---|---|---|---|
| 3/4“ copper 90 elbow | bag of 10 | 12 | $14.20 | $17.80 | $170.40 | $213.60 |
| 1/2“ PVC Sch 40 | 10 ft length | 60 | $3.85 | $4.60 | $231.00 | $276.00 |
| 12/2 Romex | 250 ft roll | 6 | $89.00 | $104.50 | $534.00 | $627.00 |
| 1/2“ EMT set screw connector | bag of 100 | 8 | $39.68 | $41.20 | $317.44 | $329.60 |
| 20A single pole breaker | each | 25 | $8.90 | $9.35 | $222.50 | $233.75 |
| Basket total | $1,475.34 | $1,679.95 |
Index = 1,679.95 / 1,475.34 x 100 = 113.9. Your basket costs 13.9% more than it did in the base month.
Note what the weighting did, and did not, do here. The unweighted average of the five percentage changes is 14.2%, against 13.9% weighted. Close, because in this basket the biggest movers also happen to be big spend lines. When they are not, the two numbers separate fast, and the weighted one is the one that matches your bank account. Weight anyway, because you cannot know in advance which month is which.
The single most useful column is the per-SKU percentage change, not the index: +25.4% on the copper elbow, +19.5% on the PVC, +17.4% on the wire, +3.8% on the connector, +5.1% on the breaker. The index is the headline. The per-SKU list is the work order, and it is where material price creep shows itself line by line.
Step 5: separate a price move from a mix change
Your index only moves on price, by construction. But your actual material cost moves on both price and quantity, and when the owner asks why the job ran over you need to say which. Cost accounting already has the split, and the definitions are worth using exactly as written.
Materials price variance = (actual price minus standard price) times actual quantity. Using the copper elbow above: ($17.80 minus $14.20) x 12 bags = $43.20 unfavorable for the month, on one SKU. Named causes of an unfavorable price variance include rush delivery charges, commodity price swings, supplier pricing power, and volume differing from the estimate.
Materials quantity variance = (actual quantity used minus standard quantity expected) times standard price. That is the second half of the split, and total direct materials cost variance is (actual quantity x actual price) minus (standard quantity x standard price).
Your basket index is a clean read on the first term with the second held still. That is precisely why it is worth building separately from your job costing: job cost data mixes the two together, and the mix hides the price signal.
Step 6: compare your index to the public series
Once you have a monthly number, put it next to a matched public series. Not the headline construction basket, which carries a lot of diesel. The narrow one for the family.
| Your basket line | Matched BLS series |
|---|---|
| Copper fittings, copper tube | Copper and brass mill shapes (WPU102502) |
| Building wire and cable | Copper wire and cable (WPU10260314) |
| PVC and CPVC pipe and fittings | Plastics pipe (WPU07210603) and plastics pipe fittings and unions (WPU07210604) |
| Strut, conduit, steel fabrications | Steel mill products (WPU1017) |
| Ductwork and sheet metal | Sheet metal products (WPU1073) |
| HVAC equipment | Unitary air-conditioners, except air source heat pumps (WPU114802) or heat pumps (WPU11480734) |
| Fixtures, faucets and trim | Plumbing fixture fittings and trim (WPU1054) |
Compare percentage changes over the same two months, never the index levels. The series are anchored to different base periods (WPU102502 is based to 1982, WPU07210603 to December 2005), so the levels are on different scales and mean nothing side by side. On wire, note that WPU10260314 is a genuine commodity series based to December 1986 rather than an industry proxy, but it covers wire and cable as a category and does not separate THHN from MC from NM-B, so your own roll SKUs can drift from it. Two of these lines need a matched pair rather than one series. Plastic pipe splits into pipe (WPU07210603) and fittings and unions (WPU07210604), so match whichever your basket actually carries, and neither separates PVC from CPVC from PEX. HVAC splits into unitary air-conditioners (WPU114802) and heat pumps (WPU11480734), and in 2026 those two moved in opposite directions, so match the one you install rather than blending them or reaching for the industry index. WPU114802 also publishes about a month behind the other series here, which matters when you are lining up percentage changes over the same two months. Recent BLS months are marked preliminary and get revised, so re-pull before you quote one.
Reading the comparison honestly:
- Your index runs with the series. The move is the market. That is a bidding and escalation problem, not a supplier problem.
- Your index runs well ahead of the series. Something local is happening: an agreement that lapsed, a branch without your rates loaded, a substituted part number, or an exhausted job quote. Take the specific SKUs, not the index, to your rep.
- Your index runs behind the series. Your agreement is holding while the market moves, which is worth knowing before you go asking for concessions.
- The series moved and your index did not, then your index jumps three months later. That is normal distributor pass-through timing. Distributors buy into stock ahead of the move.
One thing your own index cannot do: serve as the index in an escalation clause. Adjustment there is either index-based against an agreed public index or cost-based on documented actual cost, and no owner will agree to arbitrate against a number you compute yourself. Use the public series in the clause and your own index to run your shop.
What to do with it every month
- Pull the month’s invoices and record unit price, UOM, branch, and document type for each basket SKU.
- Volume-weight multiple purchases of the same SKU in the same month.
- Recompute the basket total and the index. One division.
- Sort SKUs by percentage change since base. Look at the top five.
- For each of those five, check the boring explanations first: UOM change, different branch, will-call ticket, expired job quote, substituted part number.
- Whatever survives that check is a credit request or a pricing conversation, with a date, a part number, and two prices attached. That is the same evidence a three-way match produces on the transaction side, extended over time.
- Re-base annually, and keep the old basket so you have a continuous history.
The short version
- A material price index built from invoices is a fixed basket of ten to twenty-five SKUs, one base month, unit-normalized prices, and base-month quantity weights.
- Index = current-price basket cost divided by base-price basket cost, times 100.
- Normalize the unit of measure or the whole thing is noise. Record UOM, branch, and document type with every price.
- Weight by spend. Unweighted averages let cheap parts outvote expensive ones.
- The per-SKU change list matters more than the headline index. It is the part you can act on.
- Compare percentage changes to a matched BLS series, never index levels, and treat recent readings as preliminary.
- Use the public index in escalation clauses. Use yours at the counter and at the negotiating table, where twelve months of price data is the whole point.
Sources
- Materials price variance, AccountingTools
- Direct materials variances, Penn State (Pressbooks)
- Copper and brass mill shapes (WPU102502), BLS
- Copper wire and cable (WPU10260314), BLS
- Plastics pipe (WPU07210603), BLS
- Plastics pipe fittings and unions (WPU07210604), BLS
- Plastics pipe and pipe fitting manufacturing (PCU326122326122), BLS
- Steel mill products (WPU1017), BLS
- Sheet metal products (WPU1073), BLS
- Unitary air-conditioners, except air source heat pumps (WPU114802), BLS
- Heat pumps (WPU11480734), BLS
- Air-conditioning, refrigeration and forced air heating equipment manufacturing (PCU333415333415), BLS
- Plumbing fixture fittings and trim (WPU1054), BLS
- Escalation Clause, Procore
- Return Policies, Electrical Wholesalers