> ## Documentation Index
> Fetch the complete documentation index at: https://docs.partfinder.dev/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> The PartFinder API is served at https://api.partfinder.dev. Authenticate with `Authorization: Bearer $PARTFINDER_API_KEY`; create an account at https://console.partfinder.dev/sign-up and pick pay as you go or a monthly or yearly plan (see https://partfinder.dev/pricing), then create a key. Pay as you go starts with 100 free credits, once per person and per company.
> Install the PartFinder agent skill with `npx skills add https://docs.partfinder.dev` (skill file: https://docs.partfinder.dev/skill.md).
> The OpenAPI document at https://api.partfinder.dev/v1/openapi.json is the source of truth for request and response schemas.
> What each call costs and current plans: https://api.partfinder.dev/llms.txt.

# Analyze a BOM

> Submit a bill of materials and get a risk score, drivers, gap flags and current supply for every line we can score, plus the BOM's own average, distribution and country breakdown.

One call accepts the BOM, then you poll a second for the report. This page covers the shape of
both, how to read a report while it is still processing, and what comes back for a line we could
not score.

## The call

```python Python theme={null}
import os

import requests

response = requests.post(
    "https://api.partfinder.dev/v1/bom",
    headers={
        "Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}",
        "Idempotency-Key": "6f1c2e9a-3b7d-4c55-9a0e-2d8f41b7c3e1",
    },
    json={
        "name": "Controller board rev C",
        "parts": [
            {
                "mpn": "TPS54331DR",
                "manufacturer": "Texas Instruments",
                "quantity": 100,
                "supplier": "Mouser",
            },
            {"mpn": "STM32F407VGT6", "quantity": 50},
            {
                "mpn": "6305-2RS",
                "manufacturer": "SKF",
                "quantity": 10,
                "description": "Deep groove ball bearing",
            },
        ],
    },
)
print(response.json())
```

```javascript JavaScript theme={null}
const response = await fetch("https://api.partfinder.dev/v1/bom", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "6f1c2e9a-3b7d-4c55-9a0e-2d8f41b7c3e1",
  },
  body: JSON.stringify({
    name: "Controller board rev C",
    parts: [
      { mpn: "TPS54331DR", manufacturer: "Texas Instruments", quantity: 100, supplier: "Mouser" },
      { mpn: "STM32F407VGT6", quantity: 50 },
      {
        mpn: "6305-2RS",
        manufacturer: "SKF",
        quantity: 10,
        description: "Deep groove ball bearing",
      },
    ],
  }),
});
console.log(await response.json());
```

```bash cURL theme={null}
curl https://api.partfinder.dev/v1/bom \
  -X POST \
  -H "Authorization: Bearer $PARTFINDER_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2e9a-3b7d-4c55-9a0e-2d8f41b7c3e1" \
  -d '{
    "name": "Controller board rev C",
    "parts": [
      { "mpn": "TPS54331DR", "manufacturer": "Texas Instruments", "quantity": 100, "supplier": "Mouser" },
      { "mpn": "STM32F407VGT6", "quantity": 50 },
      { "mpn": "6305-2RS", "manufacturer": "SKF", "quantity": 10, "description": "Deep groove ball bearing" }
    ]
  }'
```

`parts[].mpn` is the only required field. `manufacturer`, `supplier` and `description` are used
when present; a line sent without `supplier` is flagged `no_supplier` (see the gap table below).
`quantity` is stored and returned on the line; it does not affect the score today. `name` is a
label shown back in the list. `Idempotency-Key` is optional and set by your code; see below.

One call carries up to 500 lines. Two of the gap flags are relative to the whole BOM (a line is
"single source" or "concentrated" only in relation to the other lines), so splitting a BOM across
calls changes what those flags report. For a BOM over 500 lines, upload it as a file instead: a
file carries up to 2,000 rows in one BOM.

The call answers `202` at once, with two headers: `Location`, the absolute URL of the report, and
`Retry-After: 5`, the seconds to wait before polling it.

```json theme={null}
{
  "as_of": "2026-09-26T06:48:13.019Z",
  "summary": "Accepted 3 lines. The BOM is being analyzed; poll GET /v1/bom/7df7d0aa-ba87-4a66-bd6d-01c033bafe4e until status is \"complete\".",
  "bom": {
    "id": "7df7d0aa-ba87-4a66-bd6d-01c033bafe4e",
    "name": "Controller board rev C",
    "status": "processing",
    "lines": 3,
    "created_at": "2026-09-26T06:48:09.659Z"
  }
}
```

Poll `GET /v1/bom/{id}` until `bom.status` is `complete`; every five seconds is a good interval,
and a read while processing is cheap.

## Retrying safely

If a call times out you cannot tell whether the BOM was stored. Have your code generate a new
`Idempotency-Key` (a UUID) for each submission and send it as a header; when your code retries that
same request, it sends the same key. If the first request got through, the retry gets that BOM back
with `202` instead of a second copy, and its `summary` says the key was already used. A key is
recognised only from the same API key that sent it, so separate API keys never share keys. The
request body is not compared, which is why the key must come from your code rather than from a
person or a model. The header is optional: without it, a retried submission creates a second BOM.

Reuse a key only to retry the same submission. A new run of a BOM, or an updated version of it, is
a new call with a new key or none, and gets a new id. To see current scores for a BOM you already
submitted, read it again: scores are computed when you read. `POST /v1/bom/upload` takes the same
header.

## Uploading a file instead

If the BOM is a spreadsheet, send it as-is to `POST /v1/bom/upload` as `multipart/form-data`,
with the file in `file` and an optional `name`. We find the part-number column and the columns we
recognize, such as manufacturer, quantity, supplier and description, then run the same analysis.
A row whose part-number cell is blank is skipped, so `bom.lines` can be lower than the row count
of the file; the JSON call, by contrast, rejects a line without `mpn`.

```python Python theme={null}
import os

import requests

with open("bom.csv", "rb") as file:
    response = requests.post(
        "https://api.partfinder.dev/v1/bom/upload",
        headers={"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"},
        files={
            "file": ("bom.csv", file, "application/octet-stream"),
            "name": (None, "Controller board rev C"),
        },
    )
print(response.json())
```

```javascript JavaScript theme={null}
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("file", new Blob([await readFile("bom.csv")]), "bom.csv");
form.append("name", "Controller board rev C");

const response = await fetch("https://api.partfinder.dev/v1/bom/upload", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` },
  body: form,
});
console.log(await response.json());
```

```bash cURL theme={null}
curl https://api.partfinder.dev/v1/bom/upload \
  -X POST \
  -H "Authorization: Bearer $PARTFINDER_API_KEY" \
  -F "file=@bom.csv" \
  -F "name=Controller board rev C"
```

```json theme={null}
{
  "as_of": "2026-09-26T06:48:13.019Z",
  "summary": "Accepted 3 lines. The BOM is being analyzed; poll GET /v1/bom/7df7d0aa-ba87-4a66-bd6d-01c033bafe4e until status is \"complete\".",
  "bom": {
    "id": "7df7d0aa-ba87-4a66-bd6d-01c033bafe4e",
    "name": "Controller board rev C",
    "status": "processing",
    "lines": 3,
    "created_at": "2026-09-26T06:48:09.659Z"
  }
}
```

CSV and XLSX are accepted; `name` defaults to the file name. One file carries up to 2,000 rows,
and a request body over 4.5 MB is refused with `413`, without the usual error body. From
here on the two paths are the same: the report, the list and the delete below apply to an
uploaded BOM as well.

## What comes back

```python Python theme={null}
import os

import requests

response = requests.get(
    "https://api.partfinder.dev/v1/bom/7df7d0aa-ba87-4a66-bd6d-01c033bafe4e",
    headers={"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"},
    params={"country": "SG"},
)
print(response.json())
```

```javascript JavaScript theme={null}
const response = await fetch("https://api.partfinder.dev/v1/bom/7df7d0aa-ba87-4a66-bd6d-01c033bafe4e?country=SG", {
  headers: { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` },
});
console.log(await response.json());
```

```bash cURL theme={null}
curl "https://api.partfinder.dev/v1/bom/7df7d0aa-ba87-4a66-bd6d-01c033bafe4e?country=SG" \
  -H "Authorization: Bearer $PARTFINDER_API_KEY"
```

```json theme={null}
{
  "as_of": "2026-09-26T06:48:30.467Z",
  "summary": "3 lines: 0 critical, 0 high, 1 medium, 1 low, 1 not scored. Flagged at medium or above: geography on 3 lines, lead-time volatility on 2 lines.",
  "bom": {
    "id": "7df7d0aa-ba87-4a66-bd6d-01c033bafe4e",
    "name": "Controller board rev C",
    "status": "complete",
    "lines": 3,
    "created_at": "2026-09-26T06:48:09.659Z",
    "completed_at": "2026-09-26T06:48:26.585Z",
    "identified": 3,
    "unresolved": 0
  },
  "scored_for_country": "Singapore",
  "risk": {
    "average": {
      "value": 27,
      "scale": 100,
      "direction": "higher_is_worse",
      "interpretation": "Medium. The mean of the 2 scored lines."
    },
    "distribution": {
      "critical": 0,
      "high": 0,
      "medium": 1,
      "low": 1,
      "not_scored": 1
    },
    "gaps": [
      {
        "kind": "no_supplier",
        "lines": 2,
        "interpretation": "2 lines name no supplier. A data gap; it counts in the gaps driver."
      }
    ]
  },
  "countries": [
    {
      "country": "Mexico",
      "country_code": "MX",
      "lines": 1,
      "share_percent": 33.3,
      "worst_tier": "low"
    },
    {
      "country": "Malaysia",
      "country_code": "MY",
      "lines": 1,
      "share_percent": 33.3,
      "worst_tier": "medium"
    },
    {
      "country": "China",
      "country_code": "CN",
      "lines": 1,
      "share_percent": 33.3,
      "worst_tier": null
    }
  ],
  "parts": [
    {
      "line": 1,
      "mpn": "TPS54331DR",
      "manufacturer": "Texas Instruments",
      "quantity": 100,
      "status": "resolved",
      "notes": [],
      "part": {
        "identified_mpn": null,
        "category": "Semiconductors",
        "lifecycle": "Active",
        "manufacturing_country": "Mexico",
        "manufacturing_country_code": "MX"
      },
      "risk": {
        "composite": {
          "value": 22,
          "scale": 100,
          "direction": "higher_is_worse",
          "interpretation": "Low. Combined from manufacturing geography, disruption exposure, lead-time volatility and sourcing gaps."
        },
        "tier": "low",
        "drivers": {
          "geography": {
            "tier": "high",
            "interpretation": "High. Reflects the risk profile of Mexico."
          },
          "disruption_exposure": {
            "tier": "low",
            "interpretation": "Low. Little exposure to active disruptions."
          },
          "lead_time_volatility": {
            "tier": "medium",
            "interpretation": "Medium. Lead times for this part vary somewhat."
          },
          "gaps": {
            "tier": "low",
            "interpretation": "Low. No sourcing gap flagged for this line."
          }
        },
        "gap_flags": [],
        "events": []
      },
      "supply": {
        "stock": {
          "value": 1108493,
          "unit": "units"
        },
        "lead_time_median": {
          "value": 63,
          "unit": "days"
        },
        "lead_time_min": {
          "value": 63,
          "unit": "days"
        },
        "lead_time_max": {
          "value": 94,
          "unit": "days"
        },
        "distributor_count": 22,
        "scope": "manufacturer",
        "distributors": [
          {
            "name": "Win Source",
            "country": "United Kingdom",
            "country_code": "GB",
            "regions": [
              "Europe"
            ],
            "subregion": "Northern Europe",
            "stock": {
              "value": 406675,
              "unit": "units"
            }
          }
        ]
      }
    }
  ]
}
```

Trimmed for length: `notice` is present on every response, the other two lines are omitted, and
`distributors` is cut to its first entry. Every field is listed in
[the reference](https://docs.partfinder.dev/api-reference/boms/get-a-bom-report.md).

`as_of` is when the response was built. Scores are computed at that moment, so it is the date the
figures are good for.

`country` is optional: the buyer's country as a two-letter ISO 3166-1 code. The scores are then
computed for a buyer there, and `scored_for_country` names it. Without it the scores are not tied
to any buyer location and `scored_for_country` is `null`. A value that is not a country code
answers `400`.

### Reading it without parsing it

`summary` is one sentence: the line count, how many lines fall in each tier, and which drivers are
at `medium` or above on how many lines, always in the same order. When any line is `high` or
`critical`, it says how many and points to `GET /v1/parts` for them. If you are relaying to a person or a model, that line is enough on its own.

### The BOM

`risk.average` is the mean of the scored lines' composites, on the same 0–100 scale as the lines
themselves; its `interpretation` says how many lines it averages. `risk.distribution` counts every
line exactly once, so its five numbers add up to `bom.lines`; `not_scored` covers lines still
pending and lines we could not score. `risk.gaps` lists each kind of gap present in this BOM
with how many scored lines carry it; a kind with no lines is not listed.

`countries` says where the BOM is made, most lines first. `share_percent` is a share of all lines,
including lines with no known origin, so the shares need not add up to 100. Lines with no known
origin are not listed. `worst_tier` is the highest tier among that country's scored lines, or
`null` when none is scored.

### Each line

`parts` is in the order you submitted, with `line` starting at 1. `part.identified_mpn` is set
only when the part number we identified differs from the one you sent.

`risk.composite` combines the four drivers under `risk.drivers`. Each driver is a band (`tier`) and a
sentence, with no number; the composite is the number:

| Driver | What it measures |
| - | - |
| `geography` | Risk carried by the country the part is made in. `null` when the origin is not known |
| `disruption_exposure` | Exposure to active disruption events and shipping conditions affecting the line's category, origin or trade routes |
| `lead_time_volatility` | How unstable the lead times quoted for it are. `null` when no lead time is quoted |
| `gaps` | Weaknesses in how this BOM sources the line, listed in `gap_flags` |

A driver with no data is `null`. The line's `risk.tier` is the band its composite falls in, and each
driver's `tier` is that driver's band, all from `low`, `medium`, `high` and `critical`. Every
`interpretation` opens with its band.

`gap_flags` uses five values:

| Flag | Meaning |
| - | - |
| `single_source` | This line's manufacturer is the only one for its category in this BOM |
| `no_supplier` | The line was sent without a `supplier`; sending one clears it |
| `concentration_risk` | The line is made in a country that supplies a large share of this BOM |
| `high_risk_region` | The line is made in a high-exposure country |
| `lifecycle_risk` | The part is end of life, obsolete or not recommended for new designs |

`supply` has the same shape as the single-part endpoint: stock and lead times as measures with
units, a `null` with a reason where nothing is quoted, and `lead_time_min` / `lead_time_max`
as the 25th and 75th percentiles of the quoted lead times (the middle half of quotes), returned as
`null` when that middle half is a single figure, since there is no range to report.
`distributor_count` is the number of distributors holding stock; `distributors` lists those holding the most, largest first.

`supply.scope` says whose listings the figures count: `manufacturer` when they are this line's
manufacturer's, `all_manufacturers` when the line names no manufacturer, and
`other_manufacturers` when none is listed for the named manufacturer, so the figures are other
makers of the same part number. It is `null` when there are no figures.

`events` lists the shipping conditions and up to five reported events that most affect the line's
`disruption_exposure`, strongest first: ordered by effect on this line's score, not by `severity`,
which is how serious the event or slowdown is in itself. Each item has a `kind` (`news`, `port_activity` or
`sea_route`) and the same shape as `disruption_events` on a part report; `id` and `url` are `null`
for shipping conditions, and `url` can be `null` on `news` too. Smaller influences are not listed,
so a line can score above 0 with `events` empty.

> [!NOTE]
>
> A line's score is its risk within this BOM. `GET /v1/parts` scores the part on its own, so the
> same part can score differently on the two.

## Line status

`status` is one of four values. `resolved` means the part was identified with a manufacturer or
a confident part number, and it carries a score unless no distributor has stock or lead-time figures
for it; that line has no overall score, its `reason` says so, and the drivers that could be
measured are still shown. `low_confidence` means we identified
something about it, at least its category, but not enough to stand behind a score. `unresolved` means it could not be identified; its `reason` reads "The part could not be
identified, so no score is computed." When the analysis could not run on a line at that moment, the
line is `unresolved` with the reason "This line could not be analyzed right now. Submit it again
later." `pending` means it is still being worked, with the reason "Still being analyzed."

`bom.identified` counts identified lines, so both `resolved` and `low_confidence`; `bom.unresolved`
counts `unresolved`. `risk.distribution.not_scored` counts every line without a score, whichever
of the three statuses it has.

## Notes on a line

We never change what you submitted. When supplier catalogues disagree with a line, the line keeps
your values and carries a note in `notes` saying what was found:

| `kind` | Meaning | `suggestions` |
| - | - | - |
| `manufacturer_not_listed` | The manufacturer on the line is not among the makers distributors list for that part number | The makers that do list it, most listed first |
| `part_not_listed` | No distributor lists that part number | Real part numbers that begin with the one submitted |
| `part_number_identified` | The line was analyzed under a different part number than the one submitted | The part number it was analyzed as |
| `part_number_needed` | What was submitted is not a manufacturer part number that can be identified, so the line is not scored | A part number the line may refer to, to confirm before using it. May be empty |

```json theme={null}
"notes": [
  {
    "kind": "part_not_listed",
    "summary": "No distributor lists NE55. Did you mean NE555N or NE556N?",
    "suggestions": ["NE555N", "NE556N"]
  }
]
```

A line is scored only on a part number you submitted. When a line carries an internal code, a
description or an incomplete part number, it is not scored under a guessed part: it gets a
`part_number_needed` or `part_not_listed` note instead, and the rest of the BOM is analyzed as usual.
Such a line returns what you submitted and the note, nothing else: its `risk.drivers` are all `null`,
`events` and `gap_flags` are empty, its `part` fields are `null`, and it counts only under
`not_scored`.
For a BOM uploaded as a file, the note's `summary` names the cell to fix, for example "Line 2
(cell A3): … Add the manufacturer part number in cell A3 of your file". Add the part numbers and
submit the BOM again, or research one part on its own with `GET /v1/parts`.

```json theme={null}
"notes": [
  {
    "kind": "part_number_needed",
    "summary": "Line 2 (cell A3): ECAP-4700uF-25V is not a manufacturer part number we can identify, so this line is not scored. Add the manufacturer part number in cell A3 of your file, or research the part on its own with GET /v1/parts. It may be UVR1E472MHD; confirm before using it.",
    "suggestions": ["UVR1E472MHD"]
  }
]
```

A note about a catalogue's answer is only written when a catalogue answered, so a temporary
supplier outage never produces one. `notes` is always present and empty when there is nothing to note. When any line has a
note, the report's `summary` says how many.

## Reading a report while it is processing

`GET /v1/bom/{id}` answers at any time. While `bom.status` is `processing`, the lines already
analyzed carry their scores and the rest are `pending` with no score, drivers, gap flags or
events; `risk`, `countries` and `summary` are computed from the analyzed lines only, and
`summary` starts with "Processing:". Because `single_source` and `concentration_risk` are
relative to the other lines, a finished line's flags and gaps driver can still move as the
remaining lines settle. When every line is identified but origin and supply are still being
gathered, `summary` says so. `status` becomes `complete` only once every line's origin and supply
are in; then `completed_at` is set and `bom.identified` plus `bom.unresolved` equal `bom.lines`.

Scores are computed when you read, against the disruption events and distributor figures current
at that moment, so two reads of the same BOM can differ.

## Listing and deleting

`GET /v1/bom` lists your organization's BOMs, newest first, 50 per page. Set `limit` for a
different page size, from 1 to 100. When `next_cursor` is not `null`, call again with `cursor` set
to it for the next page; `null` means you have them all.

```python Python theme={null}
import os

import requests

response = requests.get(
    "https://api.partfinder.dev/v1/bom",
    headers={"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"},
    params={"limit": 2},
)
print(response.json())
```

```javascript JavaScript theme={null}
const response = await fetch("https://api.partfinder.dev/v1/bom?limit=2", {
  headers: { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` },
});
console.log(await response.json());
```

```bash cURL theme={null}
curl "https://api.partfinder.dev/v1/bom?limit=2" \
  -H "Authorization: Bearer $PARTFINDER_API_KEY"
```

```json theme={null}
{
  "as_of": "2026-09-26T06:48:31.721Z",
  "summary": "2 BOMs, newest first, 0 still processing. More remain: call GET /v1/bom again with cursor set to next_cursor.",
  "boms": [
    {
      "id": "7df7d0aa-ba87-4a66-bd6d-01c033bafe4e",
      "name": "Controller board rev C",
      "status": "complete",
      "lines": 3,
      "created_at": "2026-09-26T06:48:09.659Z",
      "completed_at": "2026-09-26T06:48:26.585Z"
    },
    {
      "id": "181eff86-1651-4b9c-9344-d5c5a1c4a409",
      "name": "Scan BOM",
      "status": "complete",
      "lines": 2,
      "created_at": "2026-09-24T07:31:51.637Z",
      "completed_at": "2026-09-24T07:32:20.462Z"
    }
  ],
  "next_cursor": "MTgxZWZmODYtMTY1MS00YjljLTkzNDQtZDVjNWExYzRhNDA5"
}
```

To read every BOM, loop until `next_cursor` is `null`:

```python Python theme={null}
import os

import requests

headers = {"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"}
boms, cursor = [], None
while True:
    params = {"limit": 100, **({"cursor": cursor} if cursor else {})}
    page = requests.get("https://api.partfinder.dev/v1/bom", headers=headers, params=params).json()
    boms += page["boms"]
    cursor = page["next_cursor"]
    if cursor is None:
        break
print(len(boms))
```

```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` };
const boms = [];
let cursor = null;
do {
  const params = new URLSearchParams({ limit: "100", ...(cursor ? { cursor } : {}) });
  const page = await (await fetch(`https://api.partfinder.dev/v1/bom?${params}`, { headers })).json();
  boms.push(...page.boms);
  cursor = page.next_cursor;
} while (cursor !== null);
console.log(boms.length);
```

```bash cURL theme={null}
# Needs jq. Prints one BOM per line.
cursor=""
while :; do
  page=$(curl -s "https://api.partfinder.dev/v1/bom?limit=100${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $PARTFINDER_API_KEY")
  echo "$page" | jq -c '.boms[]'
  cursor=$(echo "$page" | jq -r '.next_cursor // empty')
  [ -z "$cursor" ] && break
done
```

A BOM is kept until you delete it. `DELETE /v1/bom/{id}` removes it and every line, and answers
with the id it removed:

```json theme={null}
{
  "as_of": "2026-09-26T06:48:32.667Z",
  "summary": "BOM 7df7d0aa-ba87-4a66-bd6d-01c033bafe4e and all of its lines were deleted.",
  "deleted": {
    "id": "7df7d0aa-ba87-4a66-bd6d-01c033bafe4e"
  }
}
```

A BOM belongs to the organization whose key submitted it. Reading or deleting one with another
organization's id answers `404`, the same as an id that does not exist.

## From BOM to part

A BOM report tells you which lines need attention; the part report tells you what to do about one.
Take the `high` and `critical` lines and call `GET /v1/parts` with each line's `mpn`, adding
`manufacturer` when the line has one. The part report adds alternates, a datasheet, the events
behind the score and recommended actions. Its score is the part's own, so expect it to differ from
the line's.

```python Python theme={null}
import os

import requests

response = requests.get(
    "https://api.partfinder.dev/v1/parts",
    headers={"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"},
    params={"mpn": "TPS54331DR", "manufacturer": "Texas Instruments"},
)
print(response.json())
```

```javascript JavaScript theme={null}
const response = await fetch("https://api.partfinder.dev/v1/parts?mpn=TPS54331DR&manufacturer=Texas%20Instruments", {
  headers: { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` },
});
console.log(await response.json());
```

```bash cURL theme={null}
curl "https://api.partfinder.dev/v1/parts?mpn=TPS54331DR&manufacturer=Texas%20Instruments" \
  -H "Authorization: Bearer $PARTFINDER_API_KEY"
```

## When the request is not right

### A line without a part number: `400`

```json theme={null}
{
  "error": "invalid_request",
  "summary": "The BOM could not be read at `parts.0.mpn`: Invalid input. Every line needs an `mpn`.",
  "reason": "Invalid input at `parts.0.mpn`."
}
```

The path names the first line that failed, counting from zero: `parts.0` is the line a report
would call `line: 1`. The whole BOM is rejected and nothing is stored or analyzed: fix that line
and send the BOM again. An empty `parts` array and a quantity that is not a positive whole number
answer the same way.

### A page request that is not right: `400`

`limit` outside 1–100, or a `cursor` that is not a `next_cursor` we returned, answers
`400 invalid_request` with a summary saying which. Pass `next_cursor` back unchanged.

### A file with no part-number column: `422`

```json theme={null}
{
  "error": "bom_columns_not_found",
  "summary": "No part-number column could be identified in \"parts.csv\". The headers seen were: Color, Weight. Name the part-number column (for example \"MPN\" or \"Part Number\") and upload again.",
  "reason": "Headers: Color, Weight."
}
```

A file that is not CSV or XLSX, or is empty, answers `400 invalid_request` naming the accepted
types. A file over 2,000 rows answers `422 bom_too_large` with the cap and the rows found.

### More than 500 lines in one call: `422`

```json theme={null}
{
  "error": "bom_too_large",
  "summary": "This BOM has 501 lines and one call takes up to 500. Send it as a file to POST /v1/bom/upload instead, which takes up to 2000 rows in one BOM; splitting it across calls would change the gap flags, which are relative to the whole BOM.",
  "reason": "Limit 500 lines per call; 501 submitted."
}
```

Nothing is stored. Send the whole BOM as a file rather than in halves, for the reason the
summary gives. A file over 2,000 rows is the one case where splitting is the answer; that 422
says so.

### No such BOM: `404`

```json theme={null}
{
  "error": "bom_not_found",
  "summary": "No BOM with id ad536207-848f-4311-8187-740d82aec2eb belongs to your organization. List yours with GET /v1/bom.",
  "reason": "Unknown id, or a BOM owned by another organization."
}
```

### The analysis could not be started: `502`

```json theme={null}
{
  "error": "generation_failed",
  "summary": "PartFinder could not start the analysis because a service did not respond. Nothing was stored — retry shortly.",
  "reason": "The analysis could not be queued."
}
```

Nothing was stored, so sending the same BOM again creates no duplicate, with or without an
`Idempotency-Key`. On the upload path the
same code and status are answered when the column detector did not respond, with `reason`
"The column detector did not respond."

> [card] [Research a part](https://docs.partfinder.dev/part-lookup.md)
>
> The single-part report; each line's supply figures share its shape.

> [card] [Errors](https://docs.partfinder.dev/errors.md)
>
> Every status code and what to do about it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.