A KiCad BOM generator should be a script, not a spreadsheet: put a manufacturer part number (MPN) on every symbol, export the grouped bill of materials with kicad-cli sch export bom, then ask DigiKey and Mouser for live stock, lead time and lifecycle on each MPN before you order. If any line cannot cover the build, the script fails and nothing ships to the fab until you fix the schematic.
I’m Jason Ratzlaff. I design boards and the software that runs on them, and the most expensive delay I see on small hardware runs is rarely the layout. It is a regulator, a connector or a crystal that was in stock the week the BOM was typed up and gone the week the order went in. This post is the workflow I use to stop that: where the data lives in KiCad, the exact export command, the distributor API calls, a grading method, alternates, and a gate that blocks the order.
Why Hand-Made BOMs Rot
A bill of materials built by copying values out of the schematic into a spreadsheet is correct for exactly one moment. After that, three things drift apart: the schematic, the spreadsheet, and the distributor shelves.
The schematic moves, the spreadsheet does not
Every late change, a resistor value tweaked during bring-up, a footprint swapped from 0603 to 0402, a pull-up added to fix a boot strap, has to be retyped by hand. Miss one and the assembler builds the old design. Reference designators are the worst: a re-annotate renumbers dozens of parts and silently breaks every row keyed on them.
Stock is a live number, not a property of the part
The quantity a distributor shows is a snapshot. A part that had tens of thousands on the shelf when you chose it can be backordered by the time the board is routed, and the page will then show a factory lead time measured in weeks or months. A spreadsheet has no way to notice. The only fix is to ask again, automatically, right before you commit money.
Hand-made BOM
spreadsheet copied from the schematic
- Source of truth
- Two copies
- After a change
- Retype by hand
- Stock check
- Once, at part choice
- Obsolete parts
- Found at order time
- Second source
- Undocumented
Scripted BOM
kicad-cli export plus live API check
- Source of truth
- One: the schematic
- After a change
- Re-run the export
- Stock check
- Every run, live
- Obsolete parts
- Flagged before ordering
- Second source
- AltMPN, auto-checked

How to Generate a BOM in KiCad That a Script Can Trust
The export is only as good as the fields behind it. KiCad symbols carry Reference, Value, Footprint and Datasheet by default, and you can add any custom field you like. For purchasing, the value “10k” is not enough: a script needs an exact orderable part.
Add MPN and Manufacturer fields to every symbol
I add the same small set of custom fields to every placed symbol and fill them in through the Symbol Fields Table (in the schematic editor’s Tools menu), which shows the whole design as one editable grid. That grid is the fastest way to see blank MPN cells before they become a problem.
| Field | What goes in it, and why the script needs it |
|---|---|
| MPN | The manufacturer’s exact orderable part number, including packaging and temperature suffixes. The key for every distributor lookup. |
| Manufacturer | The maker’s name as distributors list it. Disambiguates MPNs that collide across makers. |
| AltMPN | A pre-approved drop-in second source, or blank. Checked automatically when the primary is at risk. |
| LCSC | The LCSC / JLCPCB part number, if the board is assembled there. JLCPCB’s assembly BOM expects this column. |
| DNP attribute | KiCad’s built-in “Do not populate” flag. Lets the export drop unfitted parts cleanly. |
A lookup table that maps “10k 0402” to a part number is a second source of truth, and it drifts exactly like a spreadsheet. When the MPN lives on the symbol, copying a sub-circuit into the next design carries the orderable part with it.
Export with kicad-cli sch export bom
Current KiCad ships a command-line BOM exporter, documented in the KiCad 9.0 CLI reference. It reads the schematic directly, so there is no GUI step to forget. The options that matter for purchasing are --fields (an ordered list of fields, where ${QUANTITY} and ${DNP} are KiCad’s computed columns), --labels for the header names, --group-by to merge identical parts into one row, --exclude-dnp, and --string-delimiter. That last one is easy to miss: fields are unquoted by default, and grouped references such as C1,C2,C7 contain the same comma as the column separator.
# Export a grouped, purchasing-ready BOM straight from the schematic.
# --string-delimiter quotes grouped refs like "C1,C2,C7" so the CSV stays parseable.
# Single quotes matter: ${QUANTITY} and ${DNP} are KiCad variables, not shell ones.
kicad-cli sch export bom board.kicad_sch \
--output build/board-bom.csv \
--fields 'Reference,Value,Footprint,MPN,Manufacturer,AltMPN,LCSC,${QUANTITY},${DNP}' \
--labels 'Refs,Value,Footprint,MPN,Manufacturer,AltMPN,LCSC,Qty,DNP' \
--group-by 'MPN,Value,Footprint' \
--sort-field 'Reference' --sort-asc \
--string-delimiter '"' \
--exclude-dnp
Grouping by MPN plus Value plus Footprint means ten identical 100 nF capacitors become one row with a quantity of ten, which is exactly what a distributor lookup wants: one query per unique part, not per reference designator. If you have saved a BOM preset in the schematic’s Symbol Fields Table, --preset and --format-preset will reuse it by name instead of spelling the fields out.
What about a KiCad BOM plugin?
The older flow is plugin-based: KiCad writes an intermediate XML file and a Python script turns it into whatever format you want. The CLI still supports that with kicad-cli sch export python-bom, which produces the XML for a custom script. I only reach for it when I need structure the CSV cannot express. Plugins such as InteractiveHtmlBom are excellent for hand assembly and inspection, but they answer “where does this part go”, not “can I buy it”. For purchasing, the plain CSV is the better input.
Checking Component Availability With the DigiKey API and Mouser API
Both large distributors publish free-to-register APIs that return stock and lead time per part. I query both because each one sees a different slice of the market, and a part that is thin at one is often healthy at the other. Their published limits shape how the script is written.
A typical board has a few dozen unique lines, so a grouped BOM fits comfortably inside a day’s quota even when you run the check several times. What does not fit is querying once per reference designator on a large design, which is one more reason to group before you look anything up.
How to use the DigiKey API (Product Information v4)
Register an organization and an app on the DigiKey developer portal and subscribe it to Product Information V4. That gives you a client ID and secret. Product lookups work with the 2-legged OAuth flow: POST the ID and secret with grant_type=client_credentials to https://api.digikey.com/v1/oauth2/token and use the returned bearer token.
POST https://api.digikey.com/v1/oauth2/token
grant_type=client_credentials, client_id, client_secret
GET https://api.digikey.com/products/v4/search/{productNumber}/productdetails
Authorization: Bearer <token>
X-DIGIKEY-Client-Id: <client id>
X-DIGIKEY-Locale-Site: US
X-DIGIKEY-Locale-Currency: USD
POST https://api.mouser.com/api/v1/search/partnumber?apiKey=<key>
{"SearchByPartRequest": {"mouserPartNumber": "<MPN>",
"partSearchOptions": "Exact"}}
The call I use is ProductDetails (the second endpoint above), with the token plus an X-DIGIKEY-Client-Id header and locale headers for site and currency. It accepts a manufacturer or DigiKey part number and returns one product. The fields that matter here are QuantityAvailable, ManufacturerLeadWeeks, ProductStatus.Status, and the Discontinued and EndOfLife flags. The v4 API also exposes a Substitutions endpoint, which is a useful starting point when hunting for an alternate.
How to use the Mouser Search API
Request a Search API key from Mouser’s Search API page (it needs a My Mouser account). The key goes in the apiKey query parameter of the part-number search (the third endpoint above), and the body is a SearchByPartRequest whose partSearchOptions can be set to Exact. The published schema lists AvailabilityInStock, FactoryStock, LeadTime, LifecycleStatus, SuggestedReplacement and AvailabilityOnOrder (incoming quantities with dates) on each part. The field is named for Mouser numbers, but I pass the manufacturer part number and then keep only the result whose ManufacturerPartNumber matches exactly, because a search can return several related parts.
A healthy number at 9 a.m. can be gone by the afternoon if a large buyer places an order. Run the check immediately before you pay, and when a line is thin, buy it first, before you place the board order that depends on it.
A KiCad BOM Generator Script, Written From Scratch
Here is the whole checker. It reads the CSV from the export above, looks up each MPN at both distributors, grades the line, tries the AltMPN if the primary fails, and exits with status 1 if anything is at risk. It needs only Python 3 and requests, and it keeps credentials in environment variables so nothing secret lands in the repo.
#!/usr/bin/env python3
"""Grade every line of a KiCad BOM against live distributor stock.
Usage:
export DIGIKEY_CLIENT_ID=... DIGIKEY_CLIENT_SECRET=... MOUSER_API_KEY=...
python3 bom_check.py build/board-bom.csv --boards 25
Exits 1 when any line is AT RISK, so make or CI can block the order.
"""
import argparse, csv, os, re, sys, time, urllib.parse
import requests
DK = "https://api.digikey.com"
MOUSER = "https://api.mouser.com/api/v1/search/partnumber"
DEAD = ("obsolete", "discontinued", "not recommended", "not for new designs",
"end of life", "last time buy")
def digikey_token():
r = requests.post(f"{DK}/v1/oauth2/token", timeout=20, data={
"client_id": os.environ["DIGIKEY_CLIENT_ID"],
"client_secret": os.environ["DIGIKEY_CLIENT_SECRET"],
"grant_type": "client_credentials"})
r.raise_for_status()
return r.json()["access_token"]
def digikey(mpn, token):
path = urllib.parse.quote(mpn, safe="")
r = requests.get(f"{DK}/products/v4/search/{path}/productdetails", timeout=20, headers={
"Authorization": f"Bearer {token}",
"X-DIGIKEY-Client-Id": os.environ["DIGIKEY_CLIENT_ID"],
"X-DIGIKEY-Locale-Site": "US",
"X-DIGIKEY-Locale-Currency": "USD"})
if r.status_code == 404:
return None
r.raise_for_status()
p = r.json()["Product"]
return {"src": "DigiKey", "stock": p.get("QuantityAvailable") or 0,
"lead": p.get("ManufacturerLeadWeeks"),
"status": (p.get("ProductStatus") or {}).get("Status") or ""}
def mouser(mpn):
r = requests.post(MOUSER, params={"apiKey": os.environ["MOUSER_API_KEY"]}, timeout=20,
json={"SearchByPartRequest": {"mouserPartNumber": mpn,
"partSearchOptions": "Exact"}})
r.raise_for_status()
parts = (r.json().get("SearchResults") or {}).get("Parts") or []
hit = next((p for p in parts
if (p.get("ManufacturerPartNumber") or "").upper() == mpn.upper()), None)
if hit is None:
return None
return {"src": "Mouser", "stock": int(hit.get("AvailabilityInStock") or 0),
"lead": hit.get("LeadTime"), "status": hit.get("LifecycleStatus") or ""}
def weeks(text):
"""Lead times arrive as free text; normalize '12', '12 Weeks' or '84 Days' to weeks."""
m = re.search(r"\d+", str(text or ""))
if not m:
return None
n = int(m.group())
return n / 7 if "day" in str(text).lower() else n
def grade(need, offers, low_x=3, long_weeks=8):
live = [o for o in offers if o]
stock = max((o["stock"] for o in live), default=0)
dead = any(word in o["status"].lower() for o in live for word in DEAD)
leads = [w for w in (weeks(o["lead"]) for o in live) if w is not None]
if not live or stock < need or dead:
return "AT RISK", stock
if stock < need * low_x:
return "LOW", stock
if leads and max(leads) > long_weeks:
return "LONG LEAD", stock
return "IN STOCK", stock
def check(mpn, token):
offers = [digikey(mpn, token), mouser(mpn)]
time.sleep(2.1) # stay under Mouser's 30 calls per minute
return offers
def main():
ap = argparse.ArgumentParser()
ap.add_argument("bom")
ap.add_argument("--boards", type=int, required=True)
ap.add_argument("--attrition", type=float, default=0.05, help="extra for setup loss")
args = ap.parse_args()
token = digikey_token()
failed = 0
with open(args.bom, newline="") as f:
for row in csv.DictReader(f):
mpn = (row.get("MPN") or "").strip()
need = int(int(row["Qty"]) * args.boards * (1 + args.attrition)) + 1
if not mpn:
print(f"AT RISK {row['Refs']:<24} no MPN in schematic")
failed += 1
continue
verdict, stock = grade(need, check(mpn, token))
alt = (row.get("AltMPN") or "").strip()
note = ""
if verdict == "AT RISK" and alt:
alt_verdict, alt_stock = grade(need, check(alt, token))
note = f" alternate {alt}: {alt_verdict} ({alt_stock} in stock)"
print(f"{verdict:<10} {mpn:<28} need {need:>6} best stock {stock:>8}{note}")
failed += verdict == "AT RISK"
sys.exit(1 if failed else 0)
if __name__ == "__main__":
main()
How the quantity is computed
The script multiplies the per-board quantity by the number of boards, then adds an attrition allowance, 5% by default, because pick-and-place machines lose parts at feeder setup and on tiny passives. Assemblers publish their own loss rules, and some require extra reel length per feeder, so set --attrition to what your assembler actually asks for rather than trusting my default.
How to Grade Parts: In Stock, Low, Long Lead, At Risk
A single stock number does not tell you whether to order. I sort every line into one of four grades using three inputs: stock coverage (the best distributor stock divided by the quantity this build needs), manufacturer lead time, and lifecycle status. The thresholds are a method, not a law; tune them to your build size and risk tolerance.
| Grade | Rule in the script | What I do |
|---|---|---|
| In stock | Coverage at least 3x the build, lead time 8 weeks or less, active part | Order with the board |
| Low | Enough for this build, less than 3x | Buy it first, today |
| Long lead | Plenty now, but the factory quotes more than 8 weeks | Buy extra, or qualify an alternate |
| At risk | Not enough stock anywhere, or a lifecycle status such as obsolete, discontinued, not for new designs, last time buy or end of life | Block the order; use the AltMPN |

Why a 3x coverage threshold? Because when a distributor has only a little more than you need, one other buyer’s order can take it to zero between your check and your checkout. Why flag long lead times on parts that are in stock today? Because a first build that succeeds is usually followed by a second one, and a part with a months-long factory lead time will stall that second build even though nothing looked wrong the first time.
Alternates and Second Sources
The cheapest time to find an alternate is while you are drawing the schematic, not the day the primary disappears. An alternate only helps if it is truly drop-in, so I hold every AltMPN to the same checklist before it goes into the field.
What makes an alternate truly drop-in
- Same footprint and pinout. Same land pattern, same pin numbering, same exposed-pad connection. “Same package name” is not enough; check the mechanical drawing.
- Electrical margins that still hold. Voltage and current ratings, tolerance, temperature coefficient, and for capacitors, the capacitance left after DC bias at your operating voltage.
- Behavior at the edges. For regulators and oscillators: stability with your output capacitors, startup behavior, and enable thresholds. Two parts can share a pinout and behave very differently.
- Different supply chain. A second MPN from the same maker built in the same fab protects you from less than a part from another manufacturer.
Passives are the easy case: common chip resistors and capacitors in standard sizes have many interchangeable sources, so some teams leave the MPN generic and let the assembler choose. I still record one real MPN, because the script needs something concrete to check. Where alternates are hard, a specific microcontroller, a sensor, a shielded inductor with a particular footprint, is exactly where the AltMPN field and a design-time decision pay off.
Checking JLCPCB and LCSC parts
If JLCPCB assembles the board, its KiCad BOM guide asks for a BOM with Comment, Designator, Footprint and the JLCPCB / LCSC part number, which is why the export above carries an LCSC column. Their stock is what the assembly line will actually pull from, so check it in the JLCPCB parts library during the order upload even when DigiKey and Mouser look healthy. I do not script this step; DigiKey and Mouser both publish open, documented APIs, and the JLCPCB upload screen shows shortages line by line before you pay.
A Pre-Order Gate That Blocks Bad Orders
The checker is only useful if it runs every time, so I wire it into the build rather than relying on memory. In a Makefile, the order target depends on the stock gate, and the stock gate fails on any at-risk line.
# Makefile: the order target refuses to run until the BOM passes the stock gate
BOARDS ?= 25
build/board-bom.csv: board.kicad_sch
mkdir -p build
kicad-cli sch export bom $< --output $@ \
--fields 'Reference,Value,Footprint,MPN,Manufacturer,AltMPN,LCSC,$${QUANTITY},$${DNP}' \
--labels 'Refs,Value,Footprint,MPN,Manufacturer,AltMPN,LCSC,Qty,DNP' \
--group-by 'MPN,Value,Footprint' --string-delimiter '"' --exclude-dnp
stock-gate: build/board-bom.csv
python3 bom_check.py $< --boards $(BOARDS) > build/stock-report.txt; \
s=$$?; cat build/stock-report.txt; exit $$s
order: stock-gate
@echo "Stock gate passed for $(BOARDS) boards. Upload build/ to the fab."
$ make stock-gate BOARDS=25
IN STOCK (MPN) need 27 best stock 41200
LOW (MPN) need 132 best stock 310
LONG LEAD (MPN) need 27 best stock 2600
AT RISK (MPN) need 53 best stock 12 alternate (ALT MPN): IN STOCK (8800 in stock)
# exit status 1: make stops here and the order target never runs
The pre-order checklist
- Freeze the schematic. Tag the commit the boards will be built from, so the BOM and the fab files come from the same revision.
- Fill every MPN. Open the Symbol Fields Table and sort by MPN; any blank cell is a line the script will mark at risk.
- Export with kicad-cli. Run the export command so quantities, grouping and DNP handling come from the schematic, not from memory.
- Run the stock gate for the real quantity. Pass the number of boards you are actually building, plus the attrition your assembler requires.
- Resolve every at-risk line. Promote the AltMPN, redesign, or buy the part from another source, then re-run until the gate passes.
- Buy low-stock lines first. Order them before the board order, so a competing buyer cannot empty the shelf in between.
- Confirm at the assembler. Upload the BOM and placement file and read the assembler’s own shortage report before you pay.
- Archive the report. Commit
stock-report.txtnext to the fab files, so the next build starts with a record of what was risky last time.
If you are laying out the board as well, my notes on component placement in KiCad cover the step that comes just before this one.
Frequently Asked Questions
How do I generate a BOM in KiCad?
In the schematic editor, use the Symbol Fields Table or the BOM export to produce a CSV of fields such as Reference, Value, Footprint and quantity. For repeatable results, run kicad-cli sch export bom on the .kicad_sch file with –fields, –group-by and –exclude-dnp, so the BOM is regenerated from the schematic every time instead of edited by hand.
How do I export a BOM from KiCad for JLCPCB?
JLCPCB’s assembly BOM needs Comment, Designator, Footprint and the JLCPCB / LCSC part number. Add an LCSC field to your symbols, then export with kicad-cli, mapping Value to Comment and Reference to Designator with the –labels option, and check the assembler’s shortage report during upload.
What is the best KiCad BOM plugin?
It depends on the job. InteractiveHtmlBom is a popular choice for hand assembly and inspection because it shows where each part goes. For purchasing and stock checks, the built-in kicad-cli CSV export plus a small script is simpler and easier to automate than a plugin.
How do I get a DigiKey API key?
Create an account on the DigiKey developer portal, register an organization and an app, and subscribe the app to the Product Information V4 API. The portal then issues a client ID and client secret, which you exchange for a bearer token using the OAuth 2-legged client credentials flow.
What are the DigiKey API rate limits?
DigiKey documents a limit of 120 requests per minute and 1,000 per day for the Product Information API, and returns HTTP 429 when you exceed it. Grouping your BOM by MPN keeps a typical board well inside that budget.
How do I use the Mouser API to check stock?
Request a Search API key from Mouser, then POST a SearchByPartRequest to api.mouser.com/api/v1/search/partnumber with the key in the apiKey query parameter. Each returned part includes AvailabilityInStock, LeadTime and LifecycleStatus; Mouser allows 30 calls per minute and 1,000 per day.
How often should I check component availability?
Check when you choose parts, again when the design freezes, and always immediately before you pay for parts or assembly. Stock changes daily, so a check from last week is not a reason to skip the one today.
Need a board that ships on schedule?
I design hardware end to end, from schematic and PCB layout through firmware and edge-AI software, with a BOM that is checked against live stock before anything is ordered. Tell me what you are building.