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

# Bulk Inventory Operations

> Receive, adjust, and transfer inventory in multi-item atomic batches. All items land in one DB transaction -- any failure rolls back every item.

## Overview

Three endpoints support atomic multi-item inventory operations. Each accepts an `items[]` array and guarantees all-or-nothing behavior: every item writes in a single database transaction. If item 5 of 10 fails, items 0-4 are automatically rolled back.

| Endpoint                         | Purpose                                        | GL fires?                         |
| -------------------------------- | ---------------------------------------------- | --------------------------------- |
| `POST /v1/inventory/receive`     | Non-PO receipts (FIFO layer creation per item) | No (not PO-linked)                |
| `POST /v1/inventory/adjustments` | Cycle counts, shrinkage, revaluations          | Yes, per item in same transaction |
| `POST /v1/inventory/transfers`   | Inter-location transfers                       | No on draft; yes on submit        |

Each endpoint also accepts single-item (top-level) payloads for backward compatibility.

***

## POST /v1/inventory/receive

### When to use

Use for non-PO standalone receipts -- opening stock loads, vendor samples, or inventory counts that don't correspond to a purchase order. Each item establishes its own FIFO cost layer. For PO-linked receipts use `POST /v1/purchase-orders/{id}/receive`.

### Multi-item atomic receive

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/inventory/receive" \
    -H "Authorization: Bearer ark_live_ent_wss_abc123xyz" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: receive-batch-20260513-001" \
    -d '{
      "location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "notes": "Restocking run from supplier",
      "items": [
        {
          "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
          "quantity": 10,
          "unit_cost": 12.50,
          "notes": "Circuit Breaker 10A White"
        },
        {
          "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
          "quantity": 5,
          "unit_cost": 8.75,
          "notes": "Square D Breaker Black"
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  import Arcus from "@arcuserp/typescript-sdk";

  const client = new Arcus({ apiKey: "ark_live_ent_wss_abc123xyz" });

  const result = await client.inventory.receive({
    location_id: "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
    notes: "Restocking run from supplier",
    items: [
      {
        product_id: "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
        quantity: 10,
        unit_cost: 12.50,
        notes: "Circuit Breaker 10A White",
      },
      {
        product_id: "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
        quantity: 5,
        unit_cost: 8.75,
        notes: "Square D Breaker Black",
      },
    ],
  });
  ```

  ```python Python theme={null}
  from arcuserp import Arcus

  client = Arcus(api_key="ark_live_ent_wss_abc123xyz")

  result = client.inventory.receive(
      location_id="91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      notes="Restocking run from supplier",
      items=[
          {
              "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
              "quantity": 10,
              "unit_cost": 12.50,
              "notes": "Circuit Breaker 10A White",
          },
          {
              "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
              "quantity": 5,
              "unit_cost": 8.75,
              "notes": "Square D Breaker Black",
          },
      ],
  )
  ```
</CodeGroup>

### Response shape

```json theme={null}
{
  "object": "inventory_receive_result",
  "items": [
    {
      "index": 0,
      "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
      "location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "quantity": 10,
      "unit_cost": 12.5,
      "balance": {
        "id": "0118ff93-8f65-4a72-ae7e-ca2a75526631",
        "on_hand": 10,
        "allocated": 0,
        "available": 10,
        "total_value": "125.00",
        "avg_unit_value": "12.5000"
      },
      "transaction": {
        "id": "3096987e-b156-4ee0-b373-8056f5bd8d91",
        "type": "receiving",
        "quantity": 10,
        "unit_price": "12.5000",
        "total_price": "125.00",
        "transaction_id": "IVT-0000153"
      }
    }
  ],
  "summary": {
    "total_items": 2,
    "total_quantity": 15
  }
}
```

### Required fields per item

| Field         | Type    | Notes                                                                          |
| ------------- | ------- | ------------------------------------------------------------------------------ |
| `product_id`  | UUID    | Required per item                                                              |
| `quantity`    | integer | Must be >= 1                                                                   |
| `unit_cost`   | number  | Must be > 0. Establishes the FIFO cost layer. Zero-cost receives are rejected. |
| `location_id` | UUID    | Per-item override; falls back to root `location_id`                            |
| `notes`       | string  | Optional. Per-item notes on the transaction record.                            |

### Validation error shape (400)

If any item fails upfront validation, **no items are written** and the response includes exact `param` paths:

```json theme={null}
{
  "error": "validation_error",
  "code": "multi_item_validation_failed",
  "type": "validation_error",
  "hint": "One or more items failed validation. Fix all errors before retrying.",
  "errors": [
    {
      "index": 0,
      "param": "items[0].unit_cost",
      "hint": "unit_cost must be a finite positive number. Standalone receive establishes FIFO cost basis; zero-cost receives corrupt downstream COGS."
    }
  ]
}
```

***

## POST /v1/inventory/adjustments

### When to use

Use for cycle-count corrections, shrinkage write-downs, and inventory revaluations. A GL journal entry fires per item inside the same database transaction (Rule 21). Positive deltas (adding stock) require `unit_cost` to establish a new FIFO layer. Negative deltas consume existing FIFO layers automatically and calculate COGS from the oldest layers.

### Multi-item atomic adjustment

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/inventory/adjustments" \
    -H "Authorization: Bearer ark_live_ent_wss_abc123xyz" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: adj-cycle-count-20260513-001" \
    -d '{
      "location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "reason": "cycle_count",
      "memo": "Monthly cycle count Q2 2026",
      "items": [
        {
          "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
          "quantity_delta": 3,
          "unit_cost": 12.50,
          "notes": "Count correction -- physical count higher than system"
        },
        {
          "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
          "quantity_delta": -2,
          "notes": "Shrinkage discovered during count"
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  import Arcus from "@arcuserp/typescript-sdk";

  const client = new Arcus({ apiKey: "ark_live_ent_wss_abc123xyz" });

  const result = await client.inventory.adjustments.create({
    location_id: "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
    reason: "cycle_count",
    memo: "Monthly cycle count Q2 2026",
    items: [
      {
        product_id: "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
        quantity_delta: 3,
        unit_cost: 12.50,
        notes: "Count correction -- physical count higher than system",
      },
      {
        product_id: "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
        quantity_delta: -2,
        notes: "Shrinkage discovered during count",
      },
    ],
  });
  ```

  ```python Python theme={null}
  from arcuserp import Arcus

  client = Arcus(api_key="ark_live_ent_wss_abc123xyz")

  result = client.inventory.adjustments.create(
      location_id="91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      reason="cycle_count",
      memo="Monthly cycle count Q2 2026",
      items=[
          {
              "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
              "quantity_delta": 3,
              "unit_cost": 12.50,
              "notes": "Count correction -- physical count higher than system",
          },
          {
              "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
              "quantity_delta": -2,
              "notes": "Shrinkage discovered during count",
          },
      ],
  )
  ```
</CodeGroup>

### Response shape

```json theme={null}
{
  "object": "inventory_adjustment_result",
  "items": [
    {
      "index": 0,
      "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
      "location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "quantity_delta": 3,
      "gl_amount": 37.5,
      "journal_entry_id": "c79ab8cf-b840-471f-bea9-6a24df80f550",
      "balance": {
        "on_hand": 13,
        "allocated": 0,
        "available": 13,
        "total_value": "162.50"
      },
      "transaction": {
        "id": "10e06976-8096-48bc-bbbd-5fe71626f544",
        "type": "adjustment",
        "quantity": 3,
        "unit_price": "12.5000",
        "total_price": "37.50",
        "transaction_id": "IVT-0000155"
      },
      "consumed_layers": []
    },
    {
      "index": 1,
      "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
      "location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "quantity_delta": -2,
      "gl_amount": -17.5,
      "journal_entry_id": "a8c5c8c6-b0b0-4e63-918d-a247189735f3",
      "balance": {
        "on_hand": 3,
        "allocated": 0,
        "available": 3,
        "total_value": "26.25"
      },
      "transaction": {
        "id": "93659e88-f3ba-4709-9798-388886820ea0",
        "type": "adjustment",
        "quantity": -2,
        "unit_price": "8.7500",
        "total_price": "17.50",
        "transaction_id": "IVT-0000156"
      },
      "consumed_layers": [
        {
          "layer_id": "3de7680e-fba2-444a-a204-7290cc6f08e1",
          "qty_used": 2,
          "qty_after": 3,
          "unit_cost": 8.75,
          "layer_cogs": 17.5
        }
      ]
    }
  ],
  "summary": {
    "total_items": 2,
    "total_gl_impact": 20
  }
}
```

### Required fields per item

| Field            | Type    | Notes                                                                                                               |
| ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `product_id`     | UUID    | Required per item                                                                                                   |
| `quantity_delta` | integer | Non-zero signed integer. Positive = add stock, negative = remove stock.                                             |
| `unit_cost`      | number  | Required when `quantity_delta > 0` (new FIFO layer). Ignored for negative deltas (FIFO auto-resolves).              |
| `location_id`    | UUID    | Per-item override; falls back to root `location_id`                                                                 |
| `reason`         | string  | Per-item override; falls back to root `reason`. Common values: `cycle_count`, `shrinkage`, `revaluation`, `manual`. |
| `notes`          | string  | Optional. Combined with `memo` in the transaction record.                                                           |

### GL behavior

Each adjustment item creates its own journal entry (DR/CR balanced to the cent):

* **Positive delta** (add stock): DR Inventory Asset, CR Inventory Adjustment/Suspense. Amount = `quantity_delta * unit_cost`.
* **Negative delta** (remove stock): DR Inventory Adjustment/Shrinkage, CR Inventory Asset. Amount = FIFO COGS (oldest layers consumed first).

The `journal_entry_id` on each item links the transaction to the GL record. `gl_amount` is positive for additions, negative for removals.

### Serialized product adjustments

For serialized products, include matching serial arrays whose length equals `abs(quantity_delta)`:

```json theme={null}
{
  "product_id": "SERIALIZED_PRODUCT_UUID",
  "quantity_delta": -1,
  "serial_numbers_removed": ["SN-ABC-001"],
  "notes": "Damaged unit removed from stock"
}
```

***

## POST /v1/inventory/transfers

### When to use

Use to move inventory between locations within the same entity (internal transfer) or between entities (intercompany transfer). A transfer is created in `draft` status. GL entries fire when the transfer is submitted (internal) or shipped (intercompany).

### Multi-item atomic transfer

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "https://api.arcuserp.com/v1/inventory/transfers" \
    -H "Authorization: Bearer ark_live_ent_wss_abc123xyz" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: xfer-replenish-20260513-001" \
    -d '{
      "source_location_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      "dest_location_id": "828cabe6-3fa2-4fb7-a2af-2280bbaba335",
      "notes": "Weekly replenishment transfer to secondary warehouse",
      "items": [
        {
          "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
          "quantity": 5,
          "notes": "Circuit Breaker 10A White"
        },
        {
          "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
          "quantity": 3,
          "notes": "Square D Breaker Black"
        }
      ]
    }'
  ```

  ```typescript TypeScript theme={null}
  import Arcus from "@arcuserp/typescript-sdk";

  const client = new Arcus({ apiKey: "ark_live_ent_wss_abc123xyz" });

  const transfer = await client.inventory.transfers.create({
    source_location_id: "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
    dest_location_id: "828cabe6-3fa2-4fb7-a2af-2280bbaba335",
    notes: "Weekly replenishment transfer to secondary warehouse",
    items: [
      {
        product_id: "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
        quantity: 5,
        notes: "Circuit Breaker 10A White",
      },
      {
        product_id: "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
        quantity: 3,
        notes: "Square D Breaker Black",
      },
    ],
  });
  ```

  ```python Python theme={null}
  from arcuserp import Arcus

  client = Arcus(api_key="ark_live_ent_wss_abc123xyz")

  transfer = client.inventory.transfers.create(
      source_location_id="91aa7588-4dc7-4edd-b172-7de91f5e1f78",
      dest_location_id="828cabe6-3fa2-4fb7-a2af-2280bbaba335",
      notes="Weekly replenishment transfer to secondary warehouse",
      items=[
          {
              "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
              "quantity": 5,
              "notes": "Circuit Breaker 10A White",
          },
          {
              "product_id": "ec9288c3-12b1-474f-bb09-b0b15bb7a8ff",
              "quantity": 3,
              "notes": "Square D Breaker Black",
          },
      ],
  )
  ```
</CodeGroup>

### Response shape

```json theme={null}
{
  "data": {
    "id": "e1ce42dc-e9ae-40c4-a923-aa93100b0947",
    "entity_from_id": "65dcd234-53c3-4f54-b772-ee349528d497",
    "entity_to_id": "65dcd234-53c3-4f54-b772-ee349528d497",
    "location_from_id": "91aa7588-4dc7-4edd-b172-7de91f5e1f78",
    "location_to_id": "828cabe6-3fa2-4fb7-a2af-2280bbaba335",
    "transfer_number": "SB-TRF-0000001",
    "status": "draft",
    "transfer_type": "internal",
    "notes": "Weekly replenishment transfer to secondary warehouse",
    "gl_posted": false,
    "items": [
      {
        "id": "d8c6bb7a-9c4c-446a-8518-d6f9398dbedc",
        "transfer_id": "e1ce42dc-e9ae-40c4-a923-aa93100b0947",
        "product_id": "7f6f3e14-eaad-4c45-b922-209f6dc6d140",
        "quantity_sent": "5.0000",
        "quantity_received": "0.0000",
        "notes": "Circuit Breaker 10A White"
      }
    ]
  }
}
```

### Field name aliases

Both naming conventions are accepted:

| Canonical            | Alias              |
| -------------------- | ------------------ |
| `source_location_id` | `from_location_id` |
| `dest_location_id`   | `to_location_id`   |

### GL lifecycle

| Status                    | GL action                                                       |
| ------------------------- | --------------------------------------------------------------- |
| `draft`                   | No GL. Items stored; no stock movement yet.                     |
| `submitted` (internal)    | GL fires: DR Transit Inventory / CR Source Warehouse Inventory. |
| `shipped` (intercompany)  | GL fires: DR Due From / CR Source Inventory.                    |
| `received` (intercompany) | GL fires: DR Dest Inventory / CR Due To.                        |

***

## Atomicity guarantee

All three endpoints wrap their `items[]` loop in a single `pool.connect()` + `BEGIN/COMMIT/ROLLBACK` transaction:

1. All items validate upfront (before `BEGIN`).
2. `BEGIN` starts.
3. Each item writes inside the transaction.
4. Any item failure triggers `ROLLBACK` -- every item reverts.
5. `COMMIT` fires only after all items succeed.

On failure, the response includes `failed_item_index` and `items_completed_before_failure` so you know which item triggered the rollback. All items are reverted regardless of `items_completed_before_failure` -- the count is informational only.

## Idempotency

All three endpoints honor the `Idempotency-Key` header. Replaying the same key within 24 hours returns the original response without re-applying the operation. Use a UUID or a deterministic key tied to your batch (e.g. `receive-batch-${date}-${batchId}`).

## Related endpoints

* `POST /v1/purchase-orders/{id}/receive` -- PO-linked receipts (triggers GRNI GL, matches against PO lines)
* `GET /v1/inventory/balances` -- query current on\_hand / allocated / available by product and location
* `GET /v1/inventory/transactions` -- movement history log
* `GET /v1/inventory/fifo-layers` -- FIFO cost layers per product/location
* `POST /v1/inventory/serials` -- bulk enroll serial numbers (status transition only, no on\_hand change)
