> ## 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.

# Use PartFinder as an agent tool

> Wire the PartFinder API into an agent: generate the tool from our OpenAPI document, hand the response straight to the model, and handle refusals without inventing parts.

For developers building PartFinder into their own product's assistant. Using an existing agent such as
Claude Code? See [MCP server](https://docs.partfinder.dev/mcp-server.md) and [Agent skills](https://docs.partfinder.dev/agent-skills.md).

The API is built for a model to read. Every score carries its scale, direction and a sentence;
every measure carries its unit; unknown values are explicit nulls with a reason; and every
response, errors included, carries a `summary` line your agent can relay verbatim.

## Build the tools from the OpenAPI document

Generate your tool definitions from our OpenAPI document,
`https://api.partfinder.dev/v1/openapi.json`. It always matches what is live. A hand-written definition
drifts the first time we add a parameter, and you will not find out until a call fails.

`partfinder_tools.py` (Python) and `partfinder-tools.mjs` (JavaScript) read the OpenAPI document and
turn each operation into a tool: its operation id as the name, its description, and one JSON Schema for
its parameters and request body. Their `call_tool` / `callTool` makes the call a tool call names, with
the key from `PARTFINDER_API_KEY`. When a job answers `202`, it reads the job every `Retry-After` seconds
until it is no longer `processing`, so the model gets the finished result. Key and webhook operations, header parameters and the file upload are left out
of the tools.

```python Python theme={null}
# partfinder_tools.py
import json
import os
import time
from urllib.parse import quote

import requests

API = "https://api.partfinder.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['PARTFINDER_API_KEY']}"}

doc = requests.get(f"{API}/v1/openapi.json").json()


# Inline every "$ref" so each tool's schema stands alone.
def inline(s):
    if isinstance(s, list):
        return [inline(v) for v in s]
    if isinstance(s, dict) and "$ref" in s:
        return inline(doc["components"]["schemas"][s["$ref"].split("/")[-1]])
    if isinstance(s, dict):
        return {k: inline(v) for k, v in s.items()}
    return s


# One tool per operation. Keys and webhooks stay with your own code, and so does the file upload.
operations = []
for path, methods in doc["paths"].items():
    for method, op in methods.items():
        if "Keys" in op["tags"] or "Webhooks" in op["tags"]:
            continue
        content = op.get("requestBody", {}).get("content")
        if content and "application/json" not in content:
            continue
        params = [p for p in op.get("parameters", []) if p["in"] != "header"]
        body = inline(content["application/json"]["schema"]) if content else {}
        operations.append({
            "name": op["operationId"],
            "description": op.get("description") or op.get("summary"),
            "method": method.upper(),
            "path": path,
            "body_fields": list(body.get("properties", {})),
            "parameters": {
                "type": "object",
                "properties": {
                    **{p["name"]: {**p["schema"], "description": p["description"]} for p in params},
                    **body.get("properties", {}),
                },
                "required": [p["name"] for p in params if p.get("required")] + body.get("required", []),
            },
        })


def status_of(result):
    return result.get("status") or (result.get("bom") or {}).get("status")


# Make the call a tool call names. A job answers 202: read it until it is no longer processing.
def call_tool(name, args):
    op = next(o for o in operations if o["name"] == name)
    path = op["path"]
    query, body = {}, {}
    for key, value in (args or {}).items():
        if value is None:
            continue
        if f"{{{key}}}" in path:
            path = path.replace(f"{{{key}}}", quote(str(value), safe=""))
        elif key in op["body_fields"]:
            body[key] = value
        else:
            query[key] = str(value).lower() if isinstance(value, bool) else str(value)
    res = requests.request(
        op["method"],
        f"{API}{path}",
        headers=HEADERS,
        params=query,
        json=body if op["body_fields"] else None,
    )
    result = res.json()
    location = res.headers.get("location", res.url)
    while res.status_code == 202 or status_of(result) == "processing":
        time.sleep(float(res.headers.get("retry-after", 5)))
        res = requests.get(location, headers=HEADERS)
        result = res.json()
    return json.dumps(result)
```

```javascript JavaScript theme={null}
// partfinder-tools.mjs
const API = "https://api.partfinder.dev";
const headers = { Authorization: `Bearer ${process.env.PARTFINDER_API_KEY}` };

const doc = await (await fetch(`${API}/v1/openapi.json`)).json();

// Inline every "$ref" so each tool's schema stands alone.
const inline = (s) =>
  Array.isArray(s) ? s.map(inline)
  : s?.$ref ? inline(doc.components.schemas[s.$ref.split("/").pop()])
  : s && typeof s === "object" ? Object.fromEntries(Object.entries(s).map(([k, v]) => [k, inline(v)]))
  : s;

// One tool per operation. Keys and webhooks stay with your own code, and so does the file upload.
export const operations = [];
for (const [path, methods] of Object.entries(doc.paths)) {
  for (const [method, op] of Object.entries(methods)) {
    if (op.tags.includes("Keys") || op.tags.includes("Webhooks")) continue;
    const content = op.requestBody?.content;
    if (content && !content["application/json"]) continue;
    const params = (op.parameters ?? []).filter((p) => p.in !== "header");
    const body = inline(content?.["application/json"].schema) ?? {};
    operations.push({
      name: op.operationId,
      description: op.description ?? op.summary,
      method: method.toUpperCase(),
      path,
      bodyFields: Object.keys(body.properties ?? {}),
      parameters: {
        type: "object",
        properties: {
          ...Object.fromEntries(params.map((p) => [p.name, { ...p.schema, description: p.description }])),
          ...body.properties,
        },
        required: [...params.filter((p) => p.required).map((p) => p.name), ...(body.required ?? [])],
      },
    });
  }
}

const statusOf = (result) => result.status ?? result.bom?.status;

// Make the call a tool call names. A job answers 202: read it until it is no longer processing.
export async function callTool(name, args) {
  const op = operations.find((o) => o.name === name);
  let path = op.path;
  const query = new URLSearchParams();
  const body = {};
  for (const [key, value] of Object.entries(args ?? {})) {
    if (value == null) continue;
    if (path.includes(`{${key}}`)) path = path.replace(`{${key}}`, encodeURIComponent(value));
    else if (op.bodyFields.includes(key)) body[key] = value;
    else query.set(key, String(value));
  }
  const search = query.toString();
  let res = await fetch(`${API}${path}${search ? `?${search}` : ""}`, {
    method: op.method,
    headers: op.bodyFields.length ? { ...headers, "Content-Type": "application/json" } : headers,
    body: op.bodyFields.length ? JSON.stringify(body) : undefined,
  });
  let result = await res.json();
  const location = res.headers.get("location") ?? res.url;
  while (res.status === 202 || statusOf(result) === "processing") {
    await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 5) * 1000));
    res = await fetch(location, { headers });
    result = await res.json();
  }
  return JSON.stringify(result);
}
```

Each of these pages passes `operations` and `call_tool` (`callTool` in JavaScript) to one SDK:

* [OpenAI tool calling](https://docs.partfinder.dev/tools-openai.md)
* [Anthropic tool calling](https://docs.partfinder.dev/tools-anthropic.md)
* [AI SDK by Vercel](https://docs.partfinder.dev/tools-ai-sdk.md)

## Handling the response

Pass the JSON back to the model as the tool result. It needs no preprocessing.

A few things worth handling in code or in your prompt rather than leaving to the model:

**An error is an answer, not an outage.** A `422` or `404` carries a `summary` explaining
what was wrong, and often carries `did_you_mean`: parts of the kind you described on a
`422`, and the listed part numbers your input is a prefix of on a `404`. Feed that back like any
other result.

**A null is information.** `"lead_time_min": { "value": null, "reason": "The middle half of quoted
lead times is a single figure, so there is no range." }` means we checked. So does
`"manufacturing_country": null`, and when that is null the geography driver has no band
(`"tier": null`) and its `interpretation` says why. A model that fills a null in itself will present a
guess as a finding.

**Relay disruption items in the order given.** `disruption_events` lists what most affects the
part's score, strongest first. An item's `severity` is how serious the event is in itself, not its
effect on this part, so a `high` item can sit below a `medium` one, and a part can carry `high`
items beside a low `disruption_exposure`. Tell your model not to re-rank the list by `severity`.

**Pass `country` when you know where the buyer is.** Send it as a two-letter code (`SG`, not
`Singapore`) when the user says where they are, or when your product already knows it. The answer
to "where can I buy this near me" is then `supply.nearby.summary`, a sentence you can relay as it
is. The score does not change with it.

## A worked exchange

**User:** "Can I still get TPS5433?"

Your agent calls `researchPart({ mpn: "TPS5433" })` and gets:

```json theme={null}
{
  "error": "part_not_found",
  "summary": "\"TPS5433\" was not found at any supplier — it looks like a partial part number. Did you mean TPS54331, TPS54332, TPS54331D?",
  "did_you_mean": ["TPS54331", "TPS54332", "TPS54331D"]
}
```

**Agent:** relays the summary, offers the three part numbers, and calls again once the user picks
one. Nothing was invented, and the user got further than "not found".

## Several parts in one question

For a few parts, ask for each one separately. Most providers emit parallel tool calls in a
single round, so three parts cost roughly the wall-clock of the slowest one. Each call is
independent, and a part that fails does not affect the others. For a whole list, submit it as a
BOM with `POST /v1/bom` (see [BOM Analysis](https://docs.partfinder.dev/bom-analysis.md)): one call, every line scored against
the rest of the BOM.

## Alternates as a tool

For "what can replace this part?", call `getAlternates` (`GET /v1/alternates`) and relay its
`summary`. Tell your model to present the results as candidates to check against the datasheets,
never as confirmed drop-in replacements. See [Find alternate parts](https://docs.partfinder.dev/alternates.md).

## Search as a tool

For "find me a part like this" or "what is there for a 3A buck converter?", call `searchParts`
(`POST /v1/search`) with the user's words as `query`. It is asynchronous like a BOM: it answers `202`
with the search's `id` and `Retry-After: 35`. Poll `getSearch` from your code until `status` is no
longer `processing`, then hand the model the result (see the polling note under BOMs below). Relay the
`summary`, and present the rows as candidates to check against their datasheets. See
[Search for parts](https://docs.partfinder.dev/search.md).

## Compare as a tool

For "is this part a match for that one?" or "how do these three differ?", call `compareParts`
(`POST /v1/compare`). When the user names the parts, send them all in `parts`, the one to compare against
first; send `mpn` alone only to compare one part with its alternates. It is asynchronous like a search:
poll `getComparison` from your code until `status` is no longer `processing`, then hand the model the
result. Relay the `summary`, and tell your model a verdict compares what the datasheets state, not a
qualification. See [Compare parts](https://docs.partfinder.dev/compare.md).

## BOMs as a tool

Four operations turn a parts list into a risk report: `analyzeBom` (`POST /v1/bom`), `getBom`,
`listBoms` and `deleteBom`. Generated from our OpenAPI document, they arrive with their parameters
and request body already described.

**Submitting is asynchronous.** `analyzeBom` answers `202` at once with the BOM's `id`, a
`Location` header pointing at the report, and `Retry-After: 5`. Poll from your code, not from the
model: a model has no clock, so it calls `getBom` back to back and spends turns. Call `getBom` with
that `id` every `Retry-After` seconds until `bom.status` is `complete`, then hand the model the
report. While it is `processing`, the report is partial and its `summary` says how far along it is,
which you can show as progress. Every BOM reaches `complete`: lines that could not be analyzed come back
with `status: unresolved` rather than holding the BOM open.

**From the BOM to a part.** When any line is `high` or `critical`, the report's `summary` ends by
saying so and naming `GET /v1/parts`. That is the next call: research those lines with
`researchPart` for their alternates, datasheet and a plain summary of the figures behind their risk.

**Ask before deleting.** `deleteBom` removes the BOM and its report for good. Tell your model to
confirm with the user before calling it.

**Files stay in your code.** A model cannot attach a file to a tool call. When your user uploads a
CSV or XLSX, send it from your own code to `POST /v1/bom/upload` (multipart, field `file`), poll `getBom`
from your code as above, then hand the model the report.

**Pages: pass the cursor back unchanged.** `listBoms` returns up to 50 BOMs and a `next_cursor`
when there are more. The next page is the same call with that cursor, copied exactly. Keep the
tool calls and their results in the conversation between turns, not only the text: a model asked
for "the next page" without the previous result in front of it will make up a cursor, and a cursor
we did not issue is refused with `400`.

## Timeouts and repeats

A part can take tens of seconds the first time it is requested, and is fast after that. Set your
client timeout to at least 90 seconds, and do not retry on timeout alone: a second call often
returns quickly.

A `429 rate_limited` means your organization has used its calls for this minute or this day (see
[Errors](https://docs.partfinder.dev/errors.md#rate-limits)). Handle it in your code, not in the model: wait the seconds in the
`Retry-After` header, then retry. If the wait is long (the daily limit), relay the `summary`
instead of waiting. To avoid the `429` altogether, read `X-RateLimit-Remaining` on each response
and slow down as it approaches zero. Each BOM poll counts toward the limit, and so does every retry
of a failed call: back off between retries of a `502` or `503` rather than retrying at once.

Retry `503 verification_unavailable` with backoff, and do not let your agent report it as the part
not existing: that answer is `404`. Do not retry `404` or `422`; they are answers.

## Idempotency keys belong to your code, not the model

`POST /v1/bom`, `POST /v1/bom/upload`, `POST /v1/search` and `POST /v1/compare` accept an optional
`Idempotency-Key` header so a retried submission does not create a second BOM, search or comparison. A key is recognised only from the API key that sent it.
Without the header everything works; a retried submission simply creates a second BOM, search or comparison. Set the header in the code that turns your model's tool call
into an HTTP request: generate a new value (a UUID) for each tool call, and send the same value
again only when that code retries the same request after a timeout.

Do not expose the header to the model as a tool argument, and do not derive it from the
conversation. A model that reuses one value for two different BOMs gets the first BOM back for the
second, because the body is not compared. If you generate your tool from our OpenAPI document,
drop header parameters from what the model sees.

> [card] [Part Research](https://docs.partfinder.dev/part-lookup.md)
>
> What the response contains, field by field.

> [card] [BOM Analysis](https://docs.partfinder.dev/bom-analysis.md)
>
> Submitting a BOM, polling it and reading the report.

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


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