Lots and FIFO Valuation

Fenicia calculates your inventory's cost using cost layers (lots) under the FIFO methodology (first in, first out). Every time stock comes in — through a purchase order receipt, a received transfer, or a return — a lot is internally created with its unit cost and quantity. When stock is consumed (for example, an order's sale), Fenicia depletes the oldest lots first.

Read-only surface

There is no public endpoint to create or cancel a lot manually. Lots are generated internally from purchase order receipts, transfers, and returns — not from this API. All endpoints on this page require the inventory:read permission.

Before you start: pagination differs from the rest of /inventory/*

Pagination trap

Most /inventory/* listings (stock, transfers, adjustments, counts) paginate 1-indexed (page=1 by default). GET /inventory/lots is the exception within the inventory domain itself: it paginates 0-indexed (page=0 by default), just like /locations/*. Its response shape is also flat{ data, total, page, limit, totalPages, hasMore } — different from the { items, pagination: {...} } used by stock, transfers, adjustments, and counts. If you share a pagination helper across inventory resources, this silently breaks the page count.

List lots

Lists FIFO cost layers with filters. Requires inventory:read.

pagenumber

Page, 0-based. Default: 0.

limitnumber

Results per page. Default: 50.

skustring

Filter by exact SKU.

locationIdstring

Filter by location.

statusstring

active, depleted, expired, or cancelled.

sourceTypestring

purchase_order, adjustment, transfer, initial, or return.

supplierIdstring

Filter by supplier.

startDatestring

ISO 8601 start date, filters by receivedAt.

endDatestring

ISO 8601 end date.

200
{
  "data": [
    {
      "id": "lot_01HZK...",
      "tenantId": "69db07c8bce4d49b18c42a49",
      "sku": "CAM-ROJO-M",
      "locationId": "loc_cedis_cdmx",
      "sourceType": "purchase_order",
      "sourceId": "po_4471",
      "supplierId": "sup_221",
      "purchaseOrderNumber": "PO-2026-0441",
      "unitCost": 210.50,
      "currency": "MXN",
      "initialQuantity": 200,
      "currentQuantity": 137,
      "reservedQuantity": 0,
      "receivedAt": "2026-06-02T09:00:00.000Z",
      "status": "active"
    }
  ],
  "total": 34,
  "page": 0,
  "limit": 50,
  "totalPages": 1,
  "hasMore": false
}

Aggregated valuation by location

Aggregated valuation of active lots, optionally filtered by location. Requires inventory:read.

locationIdstring

Restricts the aggregation to one location.

200
{
  "valuation": [
    { "locationId": "loc_cedis_cdmx", "totalQuantity": 4820, "totalValue": 1014610.00, "lotCount": 63 }
  ]
}

Tip

This aggregation only considers lots with status: 'active'. It is the correct valuation source for this domain — unlike the GET /inventory/reports?reportType=valuation report, which is currently broken (see Inventory Reports).

Query a lot

Gets a lot by its ID. Requires inventory:read.

idstringrequired

Lot ID.

200
{
  "id": "lot_01HZK...",
  "sku": "CAM-ROJO-M",
  "locationId": "loc_cedis_cdmx",
  "unitCost": 210.50,
  "currentQuantity": 137,
  "status": "active",
  "receivedAt": "2026-06-02T09:00:00.000Z"
}
404
{ "code": "not-found", "message": "Lot not found" }

Lot movements

Lists the historical consumption/reversal movements of a lot. Requires inventory:read.

idstringrequired

Lot ID.

200
{
  "movements": [
    {
      "id": "mov_01HZ...",
      "lotId": "lot_01HZK...",
      "sku": "CAM-ROJO-M",
      "locationId": "loc_cedis_cdmx",
      "type": "sale",
      "quantity": 2,
      "unitCost": 210.50,
      "referenceType": "order",
      "referenceId": "ord_9931",
      "createdAt": "2026-07-10T18:22:00.000Z"
    }
  ]
}

Weighted average cost by SKU

Weighted average cost across a SKU's active lots. Requires inventory:read.

skustringrequired

SKU to query (path).

locationIdstring

Restricts the calculation to one location.

200
{ "averageCost": 208.14, "totalQuantity": 512, "totalValue": 106567.68, "currency": "MXN" }

Información

If the SKU has no active lots, the response is 200 with zeroed values, not a 404.

Detailed valuation by SKU and location

Lot-by-lot valuation breakdown for a SKU at a specific location. Requires inventory:read.

skustringrequired

SKU to query (path).

locationIdstringrequired

Location to query. Required — the request fails with 400 if omitted.

200
{
  "sku": "CAM-ROJO-M",
  "locationId": "loc_cedis_cdmx",
  "totalQuantity": 137,
  "totalValue": 28838.50,
  "weightedAverageCost": 210.50,
  "lots": [
    {
      "lotId": "lot_01HZK...",
      "unitCost": 210.50,
      "currentQuantity": 137,
      "value": 28838.50,
      "receivedAt": "2026-06-02T09:00:00.000Z",
      "sourceType": "purchase_order",
      "sourceId": "po_4471"
    }
  ]
}

Lots by source

Lists the lots generated by a specific source (a purchase order, an adjustment, etc.). Requires inventory:read.

typestringrequired

sourceType: purchase_order, adjustment, transfer, initial, or return.

idstringrequired

Source ID (sourceId).

200
{ "lots": [ { "id": "lot_01HZK...", "sku": "CAM-ROJO-M", "sourceType": "purchase_order", "sourceId": "po_4471" } ] }

COGS of an order

Cost of goods sold (COGS) for an order, derived from sale-type lot movements. Requires inventory:read.

orderIdstringrequired

Order ID.

200
{
  "totalCOGS": 421.00,
  "items": [
    { "sku": "CAM-ROJO-M", "quantity": 2, "unitCost": 210.50, "totalCost": 421.00 }
  ]
}

Información

This COGS is calculated from the InventoryLotMovement records with referenceType: 'order' and type: 'sale' associated with the order — not from a separate summary table.

Data model

InventoryLot

tenantId, sku, productSku?, locationId, sourceType (purchase_order|adjustment|transfer|initial|return),
sourceId, supplierId?, supplierName?, purchaseOrderNumber?, unitCost (≥0), currency (default 'MXN'),
initialQuantity (≥0), currentQuantity (≥0), reservedQuantity (default 0, ≥0), receivedAt,
lotNumber?, expirationDate?, notes?, status (active|depleted|expired|cancelled, default active)

InventoryLotMovement

tenantId, lotId, sku, locationId,
type (sale|supply_consumption|transfer_out|transfer_in|adjustment|return|fulfillment_send),
quantity (≥0), unitCost (≥0),
referenceType (order|transfer|adjustment|fulfillment|purchase_order), referenceId,
destinationType? (fba|mercado_full|warehouse|store|other), destinationId?, destinationName?, createdAt

FIFO consumption rule

Lot consumption strictly follows ascending receivedAt order (the oldest lot is depleted first) within {tenantId, sku, locationId}, and only considers lots with status: 'active'. If the requested quantity exceeds the sum of currentQuantity across the available active lots, the consumption is rejected — this is the real guard against overselling in the costing subsystem, independent of the base stock field's negative-stock guard (see Error Catalog).

Not exposed as an endpoint

FIFO consumption is triggered internally (an order's sale, a transfer's receipt) — there is no public POST to invoke it directly. Do not document it as an operation you can execute yourself via the API.

Errors

CodeHTTPCause
not-found404No lot exists with that ID in your tenant.
400Missing locationId on GET /inventory/lots/sku/{sku}/valuation (required parameter).

See the Error Catalog for the errors common to the whole domain (authentication, permissions, missing tenant).

Examples

# Active lots for a SKU
curl -G https://api.fenicia.io/inventory/lots \
  -H "Authorization: Bearer fkapi_your_api_key" \
  --data-urlencode "sku=CAM-ROJO-M" \
  --data-urlencode "status=active"
 
# Detailed valuation by SKU + location
curl -G https://api.fenicia.io/inventory/lots/sku/CAM-ROJO-M/valuation \
  -H "Authorization: Bearer fkapi_your_api_key" \
  --data-urlencode "locationId=loc_cedis_cdmx"

Next steps