Guide

Assemblies

Items built from other items: how buildable_quantity is derived, what assemble and disassemble actually move, and how assemblies differ from bundles.

·9 min read ·Updated 6 Sep 2026

An assembly is an inventory item built from other inventory items. A dive set made of a mask, a snorkel and fins. A light fitting made of a housing, a driver and an LED board.

There is no separate “create assembly” call. An item becomes an assembly the moment it has its first component, and stops being one when you remove the last.

Assemblies are not bundles

Both are items made of other items, and they behave nothing alike.

BundleAssembly
What it isA selling constructA physical build
StockDerived from its partsIts own real quantity
Building itNothing to buildassemble consumes component stock
ReversibleN/Adisassemble credits components back

If your customer sells three products together as one SKU but the warehouse still picks three items, that is a bundle. If someone physically puts parts together and what leaves the shelf is a different item, that is an assembly.

Listing them

GET /assemblies

Only items with a composition appear here. An item with no components is not an assembly and is simply absent.

curl -s "https://api.stockpilot.dev/assemblies?search=DIVE-SET-01" \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET"

search matches on SKU. Results use the counted envelope: next and previous are booleans, and you page with current_page and total_pages. page_size caps at 100 here. See API conventions.

The two numbers that matter

FieldMeaning
quantityAssemblies already built and sitting in stock
buildable_quantityHow many more could be built from component stock right now

buildable_quantity is capacity, not stock. It is not included in quantity, and adding the two together will overstate what you can sell. An assembly with quantity: 2 and buildable_quantity: 40 has two on the shelf, not forty two.

buildable_quantity is the lowest buildable_from_component across the composition, and each component carries its own:

buildable_from_component = component_available / quantity_per_assembly

So the component with the smallest value is the one holding the assembly back, and you can see which without a second lookup.

buildable_quantity is only on the assemblies endpoints. It is not returned by GET /inventory or GET /inventory/get, so a stock sync built on those will never see it.

Item IDs, not product IDs

Every path parameter here is an inventory item ID, the id from GET /inventory/get . Not product_id, and for components not the composition line’s own id either.

Composing an assembly

POST /assemblies/{item_id}/components/add

The body is one object or a list of them. Each entry identifies a component by exactly one of component_id, sku or barcode, plus a quantity of at least 1.

curl -s -X POST https://api.stockpilot.dev/assemblies/70113/components/add \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '[{"sku": "MASK-01", "quantity": 1}, {"sku": "FINS-42", "quantity": 2}]'

Adding a component that is already there updates its quantity rather than failing or duplicating the line. status on each result tells you which happened: added or updated.

The response is always 200, even when every entry failed. Walk results rather than trusting the status code. Each carries index (its position in what you sent), status of added, updated or error, and error / field_errors explaining a rejection.

An assembly cannot contain itself, and cannot contain a component that already contains it. Those come back as error entries with the reason.

assembly on the response carries the assembly as it now stands with buildable_quantity already recalculated, so a successful write reads back without another call.

Change a rate with PUT /assemblies/{item_id}/components/{component_id}/update and remove a component with DELETE /assemblies/{item_id}/components/{component_id}/delete . To remove a component, use delete rather than setting its quantity to zero.

Removing the last component makes the item stop being an assembly: it drops out of GET /assemblies and carries on as an ordinary inventory item with its own stock. Nothing is deleted, and adding a component makes it an assembly again.

Building

POST /assemblies/{item_id}/assemble
curl -s -X POST https://api.stockpilot.dev/assemblies/70113/assemble \
  -H "X-CLIENT-ID: $SP_CLIENT_ID" \
  -H "X-CLIENT-SECRET: $SP_CLIENT_SECRET" \
  -H "Idempotency-Key: 6f1a6b1e-6d2e-4f7a-9a51-0f2f2c9a2b10" \
  -H "Content-Type: application/json" \
  -d '{"quantity": 4}'

This moves real stock. Each component is deducted at its quantity_per_assembly rate, the assembly’s own quantity rises by what you built, and the component costs roll into the assembly’s cost price.

The response carries assembled, the rolled unit_cost, and the refreshed assembly with its new quantity and recalculated buildable_quantity. No follow-up read needed.

A build that cannot be covered is rejected with 400 and nothing moves. The message names the blocking component and the exact gap:

Not enough component stock: Meanwell led-driver 192w (need 1998, have 16)

Read buildable_quantity first if you would rather not make the round trip.

Taking it apart

POST /assemblies/{item_id}/disassemble is the exact reverse: the assembly’s quantity drops and each component is credited back at its quantity_per_assembly rate.

You can only take apart what has been built. Asking for more than the assembly’s own quantity is a 400 and nothing moves.

The response carries disassembled and the refreshed assembly, with a lower quantity and a higher buildable_quantity, because the components are back on the shelf.

Retrying a build safely

Both build endpoints honour an Idempotency-Key request header. This is one of only three places in the API that does, alongside POST /suppliers/create.

Without the header, a retry builds again, consuming component stock a second time. Send the same key when retrying a request whose outcome you did not see, a timeout or a dropped connection, and the build happens once.

Use a fresh key per logical build, a UUID for example. Reusing a key across two builds you actually meant to make will collapse them into one.

import uuid

def build(client, item_id, quantity):
    key = str(uuid.uuid4())          # one key per logical build
    for attempt in range(3):
        r = client.post(f"/assemblies/{item_id}/assemble",
                        json={"quantity": quantity},
                        headers={"Idempotency-Key": key})
        if r.is_success:
            return r.json()
        if r.status_code == 400:     # not enough stock, retrying will not help
            raise NotEnoughComponents(r.json())
    raise RuntimeError("assemble failed")

Note the 400 short circuit. A shortfall is a business outcome, not a transient failure, and retrying it just burns rate limit.

From the terminal

Every assembly endpoint has a CLI command, and assemble and disassemble generate a fresh Idempotency-Key per invocation so an interrupted run is safe to repeat.

stockpilot assemblies list --search DIVE-SET-01
stockpilot assemblies get 70113

stockpilot assemblies components add 70113 \
  --component sku=MASK-01,quantity=1 \
  --component sku=FINS-42,quantity=2

stockpilot assemblies assemble 70113 --quantity 4
stockpilot assemblies disassemble 70113 --quantity 2

Pass --idempotency-key yourself if you are driving a retry across separate invocations, so the two calls share a key.

See rate limits and idempotency for which other calls are safe to retry, and the inventory model for how an assembly’s own stock is reported everywhere else.

A Stockpilot engineer talking through an integration with a developer

Stuck on something

Ask a person, not a search box

The API surface is wide, and some of it only makes sense once someone explains why it works that way. If a payload is not doing what you expect, or you are weighing two approaches, say so and we will look at it with you.