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

# Track orders and fulfillment

> Connect purchase order lines to shipments, received goods, and financial records

Build an order-tracking view from stable order and line IDs. One order can have
several shipments, and one shipment can contain lines from several orders.
Shipment plans, physical receipts, invoices, and payments are separate records.

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="Choose the order">
    ```bash theme={null}
    curl --fail-with-body --get \
      --data-urlencode "orderNumber=PO-2026-0042" \
      -H "Authorization: Bearer $AMBER_API_KEY" \
      "https://app.amber.ai/api/public/v1/brands/$AMBER_BRAND/orders"
    ```

    Pick a returned `id` and set `ORDER_ID`. Replace the example order number
    with yours, or use `externalOrderCode` for your ERP reference. Combine
    `status` and `supplierId` filters to narrow the result. See
    [business identifier lookups](/tutorials/find-records).
  </Step>

  <Step title="Join order lines to shipment lines">
    Use `/order-shipments?orderId=` to find the shipments; `/shipments` does not
    accept `orderId`. Filter each shipment's lines by their `orderItemId` so
    another order's quantities are not attributed to this order.

    ```javascript track-order.mjs theme={null}
    import { apiUrl, readJson, records } from "./amber-client.mjs";

    const orderId = process.env.ORDER_ID;
    if (!orderId) throw new Error("Set ORDER_ID from /orders");
    const order = await readJson(apiUrl(`orders/${encodeURIComponent(orderId)}`));
    const orderItems = new Map();
    for await (const item of records(apiUrl("order-items", { orderId }))) {
      orderItems.set(item.id, item);
    }

    const shipments = [];
    const seen = new Set();
    for await (const link of records(apiUrl("order-shipments", { orderId }))) {
      if (seen.has(link.shipmentId)) continue;
      seen.add(link.shipmentId);
      const shipment = await readJson(apiUrl(`shipments/${encodeURIComponent(link.shipmentId)}`));
      const items = [];
      for await (const item of records(apiUrl("shipment-items", { shipmentId: shipment.id }))) {
        if (item.orderItemId !== null && orderItems.has(item.orderItemId)) items.push(item);
      }
      shipments.push({ shipment, items });
    }

    const receipts = [];
    for await (const receipt of records(apiUrl("goods-receipts", { orderId }))) {
      const lines = [];
      for await (const line of records(apiUrl("goods-receipt-lines", { goodsReceiptId: receipt.id }))) {
        lines.push(line);
      }
      receipts.push({ receipt, lines });
    }

    console.log(JSON.stringify({ order, orderItems: [...orderItems.values()], shipments, receipts }, null, 2));
    ```

    ```bash theme={null}
    node track-order.mjs > order.json.tmp && mv order.json.tmp order.json
    ```

    An order with no shipments or receipts still produces a valid result with
    empty arrays. Shipment items without a matching order-line link are omitted
    from this order's `items`; do not guess their ownership from a SKU or name.
  </Step>

  <Step title="Interpret the result">
    Use `order.details` for source dates, terms, custom fields, and exact
    decimal totals. `/order-items` preserves `unitPrice` as a decimal string;
    the order's compact `lineItems` summary uses numeric prices.

    Shipment `estimatedArrivalDate` is a forecast; `actualArrivalDate` records
    arrival. Goods-receipt lines expose `qtyReceived` separately. Do not add
    received quantities from both shipment items and goods receipts as though
    they were independent deliveries.
  </Step>
</Steps>

## Add production and packing details

All collection paths below use the same brand prefix and pagination helper:

| Need | Requests |
| - | - |
| Production history | `/production-runs?orderId=`, then `/production-run-items?productionRunId=`, then `/production-run-item-revisions?productionRunItemId=` |
| Order amendments | `/order-amendments?orderId=` |
| Carton contents | `/shipment-cartons?shipmentId=`, then `/shipment-carton-items?shipmentCartonId=`; use `orderItemId` to attribute contents |
| Packing lists | `/packing-lists?orderId=`, then `/packing-list-lines?packingListId=` |
| Inventory position | `/inventory?skuId=` with optional `addressId` |

Retain retraction and revision metadata when building a current-state view;
source history is not necessarily a list of additive quantities.

## Add invoices and payment records

| Need | Requests |
| - | - |
| Agreed terms | `/payment-agreements?orderId=`, then `/payment-milestones?agreementId=` |
| Obligations and settlement records | `/payments?orderId=`, then `/payment-settlements?obligationId={payment.id}` |
| Supplier invoices | `/invoices?orderId=`, then `/invoice-lines?partnerInvoiceId={invoice.id}` |
| Receipt data behind a settlement | `/payment-receipts?documentId={settlement.receiptDocumentId}` when that ID is non-null |

`/payments` returns obligations, not proof that money moved. Preserve settlement
`track`, currency, cancellation, and hold metadata; do not infer a paid balance
by summing every record indiscriminately. Amounts on these source resources are
decimal strings. Use [document downloads](/tutorials/download-documents) for
invoice or receipt files attached to the order or payment.

For a warehouse-wide export, page each collection once and join locally to
reduce request volume. Follow [recurring export guidance](/pagination#plan-a-recurring-export)
instead of assuming that an order timestamp covers changes to every child.
