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

# Download documents and images

> Find entity attachments, preserve shared-document access, and download files through temporary URLs

Document metadata and file bytes use separate requests. First list the documents
attached to an entity, then request a temporary download URL for the file you
want. This works for product specifications and for supplier files shared on
your brand's orders or shipments.

Before you start, set your [API key and brand](/authentication), save
[`amber-client.mjs`](/pagination#reusable-nodejs-client), and use Node.js 22 or later.

<Steps>
  <Step title="Find documents for an entity">
    Set `ORDER_ID` to a returned `/orders` ID:

    ```bash theme={null}
    curl --fail-with-body --get \
      -H "Authorization: Bearer $AMBER_API_KEY" \
      --data-urlencode "subjectKind=ORDER" \
      --data-urlencode "subjectKey=$ORDER_ID" \
      --data-urlencode "limit=100" \
      "https://app.amber.ai/api/public/v1/brands/$AMBER_BRAND/documents"
    ```

    Continue through every page before choosing a document. For a Parent
    Product, use `subjectKind=PRODUCT` and its product ID. For a Product
    Option, use `PRODUCT_OPTION` and its option ID. Supply both subject fields
    together, with the same capitalization as these examples.

    The unfiltered `/documents` collection returns brand-owned native
    documents. Subject-filtered reads also include visible shared documents
    and older attachments, so use them to collect the files for an order.
  </Step>

  <Step title="Select a document and preserve its subject">
    Save the returned `id` as `DOCUMENT_ID`. IDs can contain a `legacy:` prefix;
    keep the whole string and URL-encode it when inserting it into a path.
    Set `SUBJECT_KIND=ORDER` and `SUBJECT_KEY` to the same order ID used above.

    Keep the same subject parameters on the metadata and content requests.
    They establish access to counterparty documents shared on that entity.
    A subject does not grant access to an unrelated document.
  </Step>

  <Step title="Download the bytes">
    Save this script as `download-document.mjs`. Choose a local output path,
    such as `supplier-invoice.pdf`; the script will not overwrite an existing
    file or use a remote filename as a local path.

    ```javascript download-document.mjs theme={null}
    import { open, rm } from "node:fs/promises";
    import { pipeline } from "node:stream/promises";
    import { Readable } from "node:stream";
    import { apiUrl, readJson } from "./amber-client.mjs";

    const id = process.env.DOCUMENT_ID;
    const subjectKind = process.env.SUBJECT_KIND;
    const subjectKey = process.env.SUBJECT_KEY;
    const outputPath = process.argv[2];
    if (!id || !subjectKind || !subjectKey || !outputPath) {
      throw new Error("Set DOCUMENT_ID, SUBJECT_KIND, SUBJECT_KEY and pass an output path");
    }

    const filters = { subjectKind, subjectKey };
    const content = await readJson(apiUrl(
      `documents/${encodeURIComponent(id)}/content`, filters,
    ));
    const url = new URL(content.url);
    if (url.protocol !== "https:") throw new Error("Expected an HTTPS download URL");
    const file = await fetch(url, { signal: AbortSignal.timeout(120_000) });
    if (!file.ok || !file.body) throw new Error(`Download failed: ${file.status}`);

    const output = await open(outputPath, "wx");
    try {
      await pipeline(Readable.fromWeb(file.body), output.createWriteStream());
    } catch (error) {
      await rm(outputPath, { force: true });
      throw error;
    } finally {
      await output.close();
    }
    console.log(`Saved ${outputPath}`);
    ```

    ```bash theme={null}
    node download-document.mjs supplier-invoice.pdf
    ```

    The `/content` response is JSON containing `url`, `expiresAt`, and `extent`.
    The separate storage request returns the bytes. It does **not** send your
    Amber API key. Treat the signed URL as temporary access to the file and
    avoid publishing or logging it. Request a new URL after it expires.
  </Step>
</Steps>

## Handle files that are unavailable

Some documents contain only structured data. Read their metadata, including
`dataValue`; they have no downloadable content and `/content` returns `404`.
A `404` can also mean the document or its parent is unavailable, so do not
classify every `404` as a data-only document. A storage service failure returns
`502`; the shared client retries transient API failures.

Superseded records are hidden by default. To retrieve history, send
`includeSuperseded=true` on the list, detail, and content requests. For the
script above, add it to `filters` as the string `"true"`.

## Download product images

List `/images?productId={product.id}` to read images across the product graph,
including associated Product Options, SKUs, and versions. Follow pagination,
then call `/images/{image.id}/content` for the same signed-URL response shape.
Download that URL with a separate request without the API key, as above.

The product's `documents` expansion covers Parent Product files only. To collect
files on other entities, list their documents separately. See
[Entity relationships](/entity-relationships#documents-and-downloads) for
supported subjects and [Product graphs](/product-graphs) for image coverage.
