> 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/documents.md).

# Documents

Learn how to manage technical documentation and files through the Duro API.

### Document Types

Duro supports various document types:

* CAD Files
* Technical Drawings
* Specifications
* Test Reports
* Quality Documentation

### Querying Documents

Here's how to fetch documents:

```graphql
query {
  documents(first: 10) {
    edges {
      node {
        id
        title
        fileType
        version
        createdAt
        createdBy {
          name
        }
      }
    }
  }
}
```

### Document Operations

```graphql
mutation {
  createDocument(input: {
    title: "Assembly Instructions"
    componentId: "comp_123"
    fileUrl: "https://example.com/file.pdf"
  }) {
    document {
      id
      title
    }
  }
}
```

### File Management

* Direct upload URLs
* Version control
* Access permissions
* File metadata

***

## Organization Context for Uploads

Every file Duro stores records the organization that owns it. Duro derives that organization **server-side** from the request context — it is no longer taken from the uploader's list of organization memberships.

This matters when a user belongs to more than one organization. Previously the file was tagged with the first membership returned for the uploader, and that list has no defined order, so a file uploaded into one organization's library could be recorded against an unrelated organization the uploader also belonged to. Change order export packaging reads the organization on the file record, so a mis-tagged file surfaces there as an export that fails to build.

### Selecting the organization

Send the organization you are uploading into with the `x-organization` header, the same way you do for every other API request. Uploads accept either form of the value:

* Organization UUID: `550e8400-e29b-41d4-a716-446655440000`
* Organization slug: `@acme-corp`

```bash
curl -X POST https://api.durohub.com/graphql \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_API_TOKEN' \
  -H 'x-organization: @acme-corp' \
  -H 'x-library: @acme-corp/hardware-library' \
  -d '{"query": "..."}'
```

The header is validated against the caller's memberships — you cannot tag a file with an organization you are not a member of. See [Selecting an Organization](/getting-started/authentication.md#selecting-an-organization) for how organization selection is resolved and validated.

### `UploadRequest` fields

| Field                | Status         | Notes                                                                                                                                                                                              |
| -------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orgId`              | **Deprecated** | Retained for backwards compatibility. Stop sending it and use the `x-organization` header instead; the organization on the resulting file record is resolved server-side.                          |
| `omitOrgId: Boolean` | New            | Set to `true` for an upload that should not be associated with any organization. Leave it unset (or `false`) for normal, organization-scoped uploads such as component and change order documents. |

{% hint style="warning" %}
Only set `omitOrgId: true` when you genuinely want an unscoped file. An organization-scoped feature that later reads the file — change order export packaging, for example — has no organization to work with and will not be able to include it.
{% endhint %}

{% hint style="info" %}
Files uploaded before this change may still carry an organization that does not match the library they live in. The document itself is in the right library; only the stored organization is wrong. If a change order export package fails to generate for a document that otherwise looks correct, contact Duro support so the file record can be corrected.
{% endhint %}

***

## What a Document Depicts

A document can record the component it is *of* — a drawing of a part, rather than a file merely attached to it. That relationship lives on the document component itself, in `depictsComponentId`:

```graphql
type Component {
  "The component this document depicts, if any."
  depictsComponentId: ID

  "The depicted component, resolved. null when the document depicts nothing or the depicted component no longer exists."
  depicts: Component
}

input CreateComponentInput {
  depictsComponentId: ID
}

input UpdateComponentInput {
  depictsComponentId: ID
}
```

It is nullable and optional on both inputs. A document with `depictsComponentId: null` behaves exactly as documents always have — attached to whatever links to it, depicting nothing. Nothing changes for integrations that never set it.

A document depicts at most one component, while any number of components can attach it. Set it when you create the document, or later with the `component.update` mutation:

```graphql
mutation SetDepicts {
  component {
    update(inputs: [{
      id: "123e4567-e89b-12d3-a456-426614174000"
      depictsComponentId: "987fcdeb-51a2-43d1-9f12-426614174999"
      expectedVersion: 1
    }]) {
      id
      depictsComponentId
    }
  }
}
```

{% hint style="info" %}
`depictsComponentId` answers "which component is this document about, *now*". It is a live pointer, so it follows the component through a renumber. If you need the CPN and revision that were stamped into the artifact when it was generated, read those from the document's own recorded values — after a renumber the two legitimately differ, and the stamped value is what is true of the paper.
{% endhint %}

### Reading the depicted component

`depicts` resolves `depictsComponentId` to the component itself, so you can render "Drawing of PART-123" without a second query:

```graphql
query DocumentDepicts {
  component {
    findOne(id: "123e4567-e89b-12d3-a456-426614174000") {
      id
      name
      depictsComponentId
      depicts {
        id
        identifier {
          displayValue
        }
        revisionValue
        status {
          id
          name
        }
      }
    }
  }
}
```

`identifier.displayValue` is the depicted component's CPN. `depicts` is read-only — set the relationship through `depictsComponentId` on the create and update inputs. Because it is the same live pointer, it reflects the depicted component's *current* CPN, `revisionValue` and `status` rather than the values stamped into the document when it was generated. Compare the document's own `revisionValue` and `status` with those of `depicts` to explain a `generatedFromOlderRevision` result — see [Per-Link Staleness](#per-link-staleness).

`depicts` is `null` when `depictsComponentId` is `null`, and also when the depicted component has since been archived or no longer exists in the library. Do not assume a non-null `depictsComponentId` means a non-null `depicts`.

{% hint style="info" %}
Pointing a document at a component is a write to that component's documents. Setting or changing `depictsComponentId`, and any later update to a document that depicts a component, is checked against the depicted component's `documents` access scope — see [Who May Change a Component's Documents](#who-may-change-a-components-documents).
{% endhint %}

## Per-Link Staleness

`DocumentLink` reports whether the document is out of date on that particular link:

```graphql
type DocumentLink {
  "The link pins a document version older than the document's current version."
  pinnedDocumentOutdated: Boolean

  "The depicted component has moved on since the document was generated."
  generatedFromOlderRevision: Boolean
}
```

| Field                        | Meaning                                                                                                                                    | `null` when                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `pinnedDocumentOutdated`     | The link pins a specific document version and the document has since advanced past it — the link points at v1 while the document is at v3. | The link does not pin a version, so there is nothing to compare.    |
| `generatedFromOlderRevision` | The document depicts a component whose revision or status has moved on since the document was generated from it.                           | The document depicts nothing, or the link is not the depicting one. |

Both values are **computed on read, never stored**. There is no flag to set and none to clear: re-uploading a file or advancing a revision changes the answer on the next query, because the answer is always a comparison against current state.

Staleness is **per link**. The same document can be current on one parent and out of date on another, and only the depicting link reports `generatedFromOlderRevision` — attachments of the same file elsewhere return `null` for it. Read both fields on each link you render rather than deriving a single verdict for the document.

{% hint style="warning" %}
Treat `null` as "not applicable", not as "up to date". A `null` means the comparison does not apply to that link — a link with no pinned version and no depicts relationship returns `null` for both fields.
{% endhint %}

## Who Created a Document Link

`DocumentLink` records which integration, if any, created it, and whether the principal making the current request is that creator:

```graphql
type DocumentLink {
  "The integration that created this link. null when it was created natively in Duro."
  createdByIntegrationId: ID

  "true when the principal making this request created this link: the acting integration, or native Duro when the request acts for none."
  authoredByActingIntegration: Boolean!
}
```

| Field                         | Meaning                                                                                                                                              | `null` when                                                                                                            |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `createdByIntegrationId`      | The integration that created the link — a drawing pushed by a CAD connector, for example. Written once when the link is created and never rewritten. | The link was created natively in Duro (web app, or an API call with no acting integration), or it predates this field. |
| `authoredByActingIntegration` | Whether the link's creator is the principal making this request.                                                                                     | Never. It is a non-null `Boolean`.                                                                                     |

How `authoredByActingIntegration` answers depends on who is asking:

| The request                                           | `true` for                      | `false` for                                                |
| ----------------------------------------------------- | ------------------------------- | ---------------------------------------------------------- |
| Acts for an integration (sends `x-integration-key`)   | Links that integration created. | Links created by another integration, or natively in Duro. |
| Acts for no integration (the web app, a user API key) | Links created natively in Duro. | Links created by any integration.                          |

A request with no acting integration is treated as native Duro, and native Duro is the creator of every link whose `createdByIntegrationId` is `null`. If Duro cannot resolve its own principal, the field answers `false` rather than guess.

This is the document counterpart of [`AssemblyLink.createdByIntegrationId`](/core-concepts/components.md#bom-line-authorship), and the same reading applies: `null` means *"not attributable to an integration"*, not *"unowned"*. Both values describe the **link**, not the document — the same document attached to two components can carry a different creator on each link.

The practical use is deciding which links are yours to manage. When a connector re-syncs a component's drawings, act only on the links where `authoredByActingIntegration` is `true` and leave documents uploaded in Duro, or by another integration, alone.

{% hint style="warning" %}
Links created before this field existed carry `createdByIntegrationId: null`, including links a connector created itself. To that connector they answer `authoredByActingIntegration: false`, exactly like a link made in Duro. A connector will not recognise its own older links from this field alone.
{% endhint %}

Select them wherever a `DocumentLink` is returned, alongside the staleness fields:

```graphql
fragment DocumentLinkProvenance on DocumentLink {
  id
  createdByIntegrationId
  authoredByActingIntegration
  pinnedDocumentOutdated
  generatedFromOlderRevision
}
```

{% hint style="warning" %}
These fields are **provenance, not authority**. They record who created the link; they do not grant or restrict access, and Duro does not refuse a write based on them. What a caller may do to a component's documents is decided by the `documents` access scope, described next.
{% endhint %}

## Who May Change a Component's Documents

Writes to a component's documents answer to that component's `documents` access scope — the documents twin of the `BOM` scope that governs an assembly's lines. A library admin can designate an integration as the manager of the scope; with nothing configured the scope is open and every caller that could write before still can.

The writes the scope covers:

* Creating a document link on the component, removing one, or changing which version a link pins.
* Setting or changing `depictsComponentId` so that a document depicts the component.
* Any update to a document that already depicts the component.

Read the verdict before you write, from the component whose documents you intend to change:

```graphql
query DocumentsScope {
  component {
    findOne(id: "987fcdeb-51a2-43d1-9f12-426614174999") {
      scopeAccess(scopes: [DOCUMENTS]) {
        scope
        writable
        state
        managedBy {
          id
          name
        }
        managers {
          integration {
            id
            name
          }
          linked
        }
      }
    }
  }
}
```

The fields, the designation model and how a library configures a scope are described under [Access scopes](/core-concepts/components.md#access-scopes) on the components page; the `DOCUMENTS` scope works exactly like the `BOM` scope. `writable` depends on who is asking, so the same component can answer `true` to the managing connector and `false` to the web app in the same moment. A refused write returns one of the [access scope errors](/advanced-topics/error-handling.md#access-scope-errors).

## Synced and Pinned Links

A document link is either **synced** — it follows the document's latest version — or **pinned** to one specific version of that document. New links start synced; a link only becomes pinned when something explicitly pins it.

Change order snapshots report which of the two a link was in when the snapshot was taken:

```graphql
type DocumentLinkSnapshotItem {
  "The link follows the document's latest version rather than pinning one."
  synced: Boolean
}
```

`synced: true` and a pinned version are mutually exclusive, so a synced link never reports `pinnedDocumentOutdated` — see [Per-Link Staleness](#per-link-staleness).

### Pinning and unpinning

`documentHistoryId` on `UpdateDocumentLinkInput` carries the pin. It is nullable and optional, and the three cases are distinct:

```graphql
input UpdateDocumentLinkInput {
  "UUID pins that document version, null unpins, omitted leaves the pin as it is."
  documentHistoryId: ID
}
```

| You send                | Result                                                                         |
| ----------------------- | ------------------------------------------------------------------------------ |
| A document history UUID | The link is pinned to that version of the document.                            |
| `null`                  | The link is unpinned and goes back to following the document's latest version. |
| Nothing (field omitted) | The current pin is preserved, whether the link is pinned or synced.            |

This is the same contract as assembly links: omitting a nullable field is not the same as sending `null` for it. Build the input so an untouched pin is left out rather than serialized as `null`, or an unrelated update will silently unpin the link.

```graphql
mutation UnpinDocumentLink {
  documentLink {
    update(inputs: [{
      id: "123e4567-e89b-12d3-a456-426614174000"
      documentHistoryId: null
    }]) {
      id
    }
  }
}
```

{% hint style="info" %}
Links created through the web app before this change were pinned to the document's latest version at creation time, and those pins were not backfilled away. If a link you expected to be synced reports a pin, unpin it by sending `documentHistoryId: null`.
{% endhint %}

***

## Creating and Linking Documents with Item Mutations

The `item.create` and `item.update` mutations can create documents and link them to a component in the same transaction that writes the component itself — see [Item Mutations](/core-concepts/components.md#item-mutations-bom-and-documents-in-one-call). Three inputs cover it:

| Field                   | Type                        | On             | Description                                                                                 |
| ----------------------- | --------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| `documents`             | `[ComponentDocumentInput!]` | create, update | Creates a DOCUMENT-category component from an uploaded file and links it to this component. |
| `documentComponentIds`  | `[ID!]`                     | create, update | Links document components that already exist.                                               |
| `removeDocumentLinkIds` | `[ID!]`                     | update only    | Removes the named document links.                                                           |

```graphql
input ComponentDocumentInput {
  "The file, already uploaded, that becomes the document."
  fileId: ID!
  name: String!
  categoryId: ID
}
```

Upload the file first through the normal [upload flow](#organization-context-for-uploads) and pass only its `fileId` here.

```graphql
mutation AddDocuments {
  item {
    update(inputs: [{
      id: "123e4567-e89b-12d3-a456-426614174000"
      expectedVersion: 3
      documents: [
        { fileId: "uploaded-file-uuid", name: "Housing Drawing" }
      ]
      documentComponentIds: ["existing-document-component-uuid"]
      removeDocumentLinkIds: ["stale-link-uuid"]
    }]) {
      id
      version
    }
  }
}
```

### Why `documents` takes a file rather than a document id

A document component inherits its CPN from the parent it is created under, so it cannot exist before that parent. `documents` therefore describes a document to *create* — Duro creates the document component and links it inside the same transaction, after the parent component is written. Use `documentComponentIds` for documents that already exist.

### Additive, not desired-state

Unlike [`children`](/core-concepts/components.md#bom-children-semantics), the document fields are **additive**. `documents` and `documentComponentIds` only add links; omitting a document never unlinks it. Removal on update is explicit, through `removeDocumentLinkIds`.

This is deliberate: documents are routinely attached outside the calling tool — in the Duro web app, or by another integration — and a desired-state list would silently discard them.

{% hint style="info" %}
`removeDocumentLinkIds` is tolerant of ids that no longer exist. An already-removed link is skipped rather than failing the call, so a double-click or a concurrent removal does not roll back the rest of your save. It takes **document link** ids, not document component ids.
{% endhint %}

Each document component created this way gets exactly one history row, and the parent component still gets only one — the links do not write separate entries.

***

## Documents on Change Orders

Documents can be attached to change orders as supporting reference material. See [Attaching Documents to a Change Order](/core-concepts/change-orders.md#attaching-documents-to-a-change-order) for usage.

### Querying Documents on a Change Order

```graphql
query GetCODocuments {
  changeOrders {
    get(filter: { ids: ["123e4567-e89b-12d3-a456-426614174000"] }) {
      connection {
        edges {
          node {
            id
            name
            documents {
              id
              componentId
              componentVersion
            }
          }
        }
      }
    }
  }
}
```

### Next Steps

Learn about managing product changes with [Change Orders](/core-concepts/change-orders.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/documents.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.
