> For the complete documentation index, see [llms.txt](https://docs.durohub.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.durohub.com/core-concepts/sourcing.md).

# Sourcing

Sourcing describes how a component gets built: the manufacturer parts (MPNs) that source it, the distributor quotes (DPNs) that price each part, and which of those sources is currently primary. A component can carry multiple parts, and each part multiple quotes — that is what lets you multi-source a component instead of committing to a single vendor.

This page covers the sourcing operations with practical examples. For the exact field-by-field shape of every type, introspect the schema in [Apollo Explorer](https://api.durohub.com/graphql) — the schema descriptions there are the source of truth.

## Overview

### The Sourcing Model

```
Component
  └─ Part            an MPN from a manufacturer
       ├─ Quote       a distributor's DPN offer — pricing, MOQ, lead time
       │    └─ Document (via QuoteDocument)
       └─ Document (via PartDocument)

Component
  └─ PrimarySource   the ranked list of quotes — rank 1 is the current primary
```

| Entity          | Identity           | Description                                                                                  |
| --------------- | ------------------ | -------------------------------------------------------------------------------------------- |
| `Part`          | MPN + manufacturer | A manufacturer part that can source the component                                            |
| `Quote`         | DPN + distributor  | A distributor's offer for a part                                                             |
| `Vendor`        | —                  | Shared catalog entry; `types` says whether it acts as `MANUFACTURER`, `DISTRIBUTOR`, or both |
| `LibraryVendor` | —                  | A library's curated entry for a vendor, with an `approvalStatus`                             |
| `PrimarySource` | —                  | One entry in a component's ranked source list                                                |

### The `vendorSourcing` Namespace

All sourcing operations are namespaced: queries live under `Query.vendorSourcing` and mutations under `Mutation.vendorSourcing`. There are no root-level sourcing fields.

{% hint style="warning" %}
`mutation { applySourcingChangeset(...) }` fails. Wrap every sourcing operation:

```graphql
mutation {
  vendorSourcing {
    applySourcingChangeset(...)
  }
}
```

{% endhint %}

## Required Headers

All sourcing operations require the same headers as the rest of the API:

```bash
curl 'https://api.durohub.com/graphql' \
  -H 'x-api-key: YOUR_API_KEY' \
  -H 'x-organization: @your-org' \
  -H 'x-library: @your-org/your-library' \
  -H 'Content-Type: application/json' \
  -d '{"query": "..."}'
```

Sourcing operations never take a `libraryId` argument — the library comes from the `x-library` header.

***

## Querying Sourcing Data

### Parts on a Component

```graphql
query GetPartsForComponent {
  vendorSourcing {
    parts(filter: { componentId: "comp-123" }) {
      id
      mpn
      description
      manufacturer {
        name
      }
    }
  }
}
```

`part(id)` fetches a single part; `partHistory(partId)` returns its full version history. Parts carry `mpnUrl` — a link to the manufacturer's page for the MPN — accepted on the create and update inputs as well.

Parts also expose `isLegacyImport` (`Boolean`) — see [Legacy-import parts and quotes](#legacy-import-parts-and-quotes).

### Quotes

`quotes(partId)` lists a single part's quotes. To compare offers across every part on a component in one call, use `quotesByComponent`:

```graphql
query GetQuotesForComponent {
  vendorSourcing {
    quotesByComponent(componentId: "comp-123") {
      id
      dpn
      unitPrice
      leadTimeDays
      minQuantity
      distributor {
        name
      }
      part {
        mpn
        manufacturer {
          name
        }
      }
    }
  }
}
```

Quote fields that drive sourcing decisions:

| Field             | Type      | Description                                                                                                                |
| ----------------- | --------- | -------------------------------------------------------------------------------------------------------------------------- |
| `unitPrice`       | `Float`   | Price per unit. Nullable — a quote can be recorded before its price is known                                               |
| `minQuantity`     | `Int`     | The distributor's minimum order quantity                                                                                   |
| `maxQuantity`     | `Int`     | The distributor's cap, if any                                                                                              |
| `packageQuantity` | `Int`     | Units per reel, tube, or tray                                                                                              |
| `leadTimeDays`    | `Int`     | Distributor lead time (the part's `manufacturerLeadTimeDays` is separate)                                                  |
| `dpnUrl`          | `String`  | Link to the distributor's page for this DPN                                                                                |
| `description`     | `String`  | Free-text notes about the quote                                                                                            |
| `isLegacyImport`  | `Boolean` | `true` when the quote was created by the legacy cost migration rather than entered as a real distributor offer — see below |

{% hint style="warning" %}
`Quote.unitPrice` was previously non-nullable (`Float!`) and is now nullable. Clients that assumed a value is always present should handle `null`.
{% endhint %}

### Legacy-import parts and quotes

Before sourcing existed, a component's price lived in a single `Cost` attribute with no manufacturer or distributor attached. When that attribute was retired, those values were migrated onto the sourcing model. Because a quote requires a part and a distributor, the migration created placeholders: a sentinel distributor (`Unknown - Legacy Import`), a part whose MPN is the component's ID, and a quote carrying the old cost as its `unitPrice` with default package quantity and lead time.

`Part.isLegacyImport` and `Quote.isLegacyImport` mark those placeholder rows:

```graphql
query FindLegacySourcing {
  vendorSourcing {
    quotesByComponent(componentId: "comp-123") {
      id
      unitPrice
      isLegacyImport
      distributor {
        name
      }
      part {
        mpn
        isLegacyImport
      }
    }
  }
}
```

Both fields are read-only — they are set by the migration and are not accepted on `CreateQuoteInput`, `CreatePartInput`, or the update inputs. Anything you create through the API returns `false`.

{% hint style="info" %}
Treat a `true` flag as "the price is real, the rest of the row is not". The manufacturer, MPN, distributor, DPN, package quantity, and lead time on a legacy-import row are placeholders, so don't surface them as vendor data or use them to compare offers. Prompt users to replace the row with a real part and quote; once they do, the flag stays `false` on the new rows.
{% endhint %}

### Vendors

The org-wide catalog is `vendors` / `vendor(id)`; `manufacturers` narrows to vendors visible in the current library that can act as manufacturers. A library's own approved list is `libraryVendorList`, each entry carrying an `approvalStatus`.

Before creating a vendor, check for near-duplicates:

```graphql
query CheckVendorName {
  vendorSourcing {
    checkVendorNameSimilarity(name: "Texas Instruments") {
      name
      similarity
    }
  }
}
```

### Primary Sources and Rollup State

```graphql
query GetPrimarySources {
  vendorSourcing {
    primarySources(componentId: "comp-123") {
      id
      rank
      quote {
        dpn
        unitPrice
      }
    }
    rollupPriorityState(componentId: "comp-123") {
      prioritized
      isPrimary
    }
  }
}
```

`rollupPriorityState` reports whether a component's sourcing is prioritized within its assembly rollup and whether it is currently the primary source in that context.

***

## Adding Sourcing

### Creating Parts

`createParts` takes an **array** — batch multiple manufacturers in one call:

```graphql
mutation AddManufacturerPart {
  vendorSourcing {
    createParts(
      inputs: [
        { componentId: "comp-123", mpn: "STM32F407VGT6", manufacturerId: "vendor-st" }
      ]
    ) {
      id
      mpn
    }
  }
}
```

### Creating Quotes

`createQuotes` is also batch. On `CreateQuoteInput`, every field except `partId` is optional:

```graphql
mutation AddDistributorOffers {
  vendorSourcing {
    createQuotes(
      inputs: [
        {
          partId: "part-123"
          distributorId: "vendor-digikey"
          dpn: "497-STM32F407VGT6-ND"
          unitPrice: 8.42
          currency: "USD"
          minQuantity: 1
          leadTimeDays: 14
        }
        {
          partId: "part-123"
          distributorId: "vendor-mouser"
          dpn: "511-STM32F407VGT6"
          unitPrice: 8.15
          currency: "USD"
          minQuantity: 10
          leadTimeDays: 21
        }
      ]
    ) {
      id
      dpn
      unitPrice
    }
  }
}
```

`updatePart` / `updateQuote` edit a single row; `archivePart` / `archiveQuote` remove a row as an active source while `partHistory` / `quoteHistory` retain its record.

{% hint style="info" %}
`updatePart` and `updateQuote` are unconditional writes — no version check, no conflict error. If two callers edit the same row concurrently, the later write silently wins. When overwrite detection matters, use `applySourcingChangeset` with `expectedVersion` instead.
{% endhint %}

***

## Ranking Primary Sources

`createParts`, `createQuotes`, and `setPrimarySource` are independent calls with no cross-call atomicity — if a later step fails (for example `MAX_PRIMARY_SOURCES_REACHED`), earlier steps are not rolled back. When the group needs to succeed or fail together, use [`applySourcingChangeset`](#applying-a-sourcing-changeset).

`setPrimarySource` places a quote in the component's ranked source list; rank 1 is the primary:

```graphql
mutation RankSources {
  vendorSourcing {
    setPrimarySource(input: { componentId: "comp-123", quoteId: "quote-456", rank: 1 }) {
      id
      rank
    }
  }
}
```

* The ranked list has a per-library maximum (3 by default). At the cap, `setPrimarySource` returns `MAX_PRIMARY_SOURCES_REACHED` — un-rank an existing source first.
* `removePrimarySource(id)` takes the `PrimarySource` entry's own `id` (from `primarySources`), not the quote id.
* `reorderPrimarySources` re-ranks the existing list; `setRollupAsPrimarySource` promotes the assembly rollup itself.

***

## Applying a Sourcing Changeset

`applySourcingChangeset` stages a whole set of part/quote adds, edits, deletes, and re-ranks as a single **atomic** operation — the same mechanism the Duro app uses for its sourcing edit sessions. It returns `Boolean!` — `true` on success — and a changeset that fails validation leaves the component's sourcing unchanged.

```graphql
mutation MultiSourceComponent {
  vendorSourcing {
    applySourcingChangeset(
      componentId: "comp-123"
      changeset: {
        newParts: [
          { tempId: "p1", mpn: "STM32F407VGT6", manufacturerId: "vendor-st" }
        ]
        newQuotes: [
          {
            tempId: "q1"
            partRef: "p1"
            distributorId: "vendor-digikey"
            dpn: "497-STM32F407VGT6-ND"
            unitPrice: 8.42
            currency: "USD"
          }
        ]
        editedParts: []
        editedQuotes: []
        deletedPartIds: []
        deletedQuoteIds: []
        prioritizedOrder: ["q1"]
        primary: { kind: QUOTE, quoteRef: "q1" }
      }
    )
  }
}
```

Two patterns to know:

* **`tempId` / `Ref`** — reference a row created in the *same* call before it has a real id: `newQuotes[0].partRef` points at `newParts[0].tempId`, and `prioritizedOrder` can reference new quotes' `tempId`s. `primary.kind` is `QUOTE` or `PART` — a manufacturer part itself can be the primary when no distributor offer is ranked.
* **`expectedVersion`** — when editing an existing part or quote, pass the `version` you last read. If it no longer matches at flush time, the whole changeset is rejected (`PART_VERSION_CONFLICT` / `QUOTE_VERSION_CONFLICT`) instead of silently overwriting a concurrent change.

***

## Documents on Sourcing

Documents attach at either level: to a part (e.g. a manufacturer datasheet) via `linkDocumentToPart`, or to a quote (e.g. a distributor-specific document) via `linkDocumentToQuote`.

```graphql
mutation AttachDatasheet {
  vendorSourcing {
    linkDocumentToPart(input: { documentId: "doc-789", partId: "part-123" }) {
      id
    }
  }
}
```

`documentsByComponent(componentId)` returns every sourcing document on a component regardless of level — each result carries `linkType` (`PART` or `QUOTE`).

***

## Error Codes

Sourcing errors are `GraphQLError`s with a stable `code` in `extensions` (see [Error Handling](/advanced-topics/error-handling.md)):

| Code                                               | Meaning                                                                  |
| -------------------------------------------------- | ------------------------------------------------------------------------ |
| `DUPLICATE_PART`                                   | A part already exists for that manufacturer + MPN                        |
| `DUPLICATE_QUOTE`                                  | A quote already exists for that part + distributor + DPN + `minQuantity` |
| `PART_VERSION_CONFLICT` / `QUOTE_VERSION_CONFLICT` | `expectedVersion` mismatch — re-read and retry                           |
| `MAX_PRIMARY_SOURCES_REACHED`                      | The ranked-source list is at its per-library cap                         |
| `INVALID_QUANTITY_RANGE`                           | A quote's `minQuantity` exceeds its `maxQuantity`                        |

## Next Steps

* Evaluate sourcing changes as part of a formal review with [Change Orders](/core-concepts/change-orders.md)
* Attach datasheets and certifications with [Documents](/core-concepts/documents.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.durohub.com/core-concepts/sourcing.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
