Add a component variant to a bundle
Authorizations
A secret API key. Publishable keys cannot reach this API. A key may carry an expiry, and an expired key is refused exactly like an unknown one, with a 401 that names no reason; check the key's expires_at in the dashboard rather than inferring it from a response. When a merchant rolls a key's secret they choose a grace window of up to 3 days, and for its duration BOTH the new secret and the one it replaced authenticate, so an integration moves over on its own deploy schedule instead of at the instant the button is pressed. Move before the window closes: after it, the old secret is refused. Nothing else about this contract moves with a roll. The key keeps its id and its scopes, so the only thing an integration updates is the credential itself.
Headers
A unique key per logical write. Replaying a request with the same key returns the first response byte for byte instead of applying the write twice.
Path Parameters
Body
IT ANSWERS WITH THE WHOLE BUNDLE, 200, not with the component it created, and that is the shape the item routes rest on. A component is not addressable for reads: it is embedded in the bundle rather than served from a sub-route, and the read publishes neither its own id nor its bundle_id, so a 201 carrying {variant_id, quantity} would hand back a fragment with no self URL and nothing to re-fetch it by. 200 rather than 201 for the same reason: the resource in the body is the bundle, which already existed. ADDING A COMPONENT CHANGES WHAT EVERY OTHER COMPONENT IS WORTH PER UNIT AT CHECKOUT. The bundle carries one authoritative fixed set price and the cart allocates that one price across whatever components exist, so a new component DILUTES the others rather than adding to the bundle's price. The shopper pays the same total; the split moves. That is also why the add is on the plain write grant while removal is not published at all: dilution is undone by the reverse operation, and a removal would concentrate value into fewer components and cannot be undone from this API, because re-adding does not restore a component's original position in the allocation order. A DUPLICATE COMPONENT IS 409 item_exists, from the unique index on (bundle, variant), which is stronger convergence than the bundle create's: a retry with a FRESH key past the idempotency window collides instead of silently doubling a quantity. Change an existing component with PATCH /api/v1/bundles/{bundleId}/items/{variantId}. THERE IS NO REMOVE ROUTE, and it is a refusal rather than an omission. Dropping a component succeeds silently and reprices every survivor upward, and dropping the last one makes the bundle unpurchasable, which the storefront reports as an ordinary out of stock. apiscope publishes no write_bundles:delete to gate such a route with, so it stays a dashboard action. NO STOCK MOVES and no money moves. Components are a definition, not a reservation: what a bundle can sell is read from the component variants' inventory at cart time and is published per variant on /api/v1/inventory-levels. The parent's updated_at advances in the same transaction, which is what lets a client record the new sync timestamp without a second call. Nothing is emitted. bundle_id is refused BY NAME because the URL already names the bundle and a body disagreeing with the path would need a precedence rule nobody should have to learn; moving a component between bundles is a remove plus an add. price and price_minor are refused because a component has no price of its own, and the provenance keys (id, store_id, created_at, updated_at) because the server owns them and nothing addresses a component by its own id. No key may be null.
Required. A product variant in THIS store, the same id the products resource publishes inside its variants array. It is checked under Row-Level Security rather than by a foreign key alone, so a variant belonging to another merchant reads as absent and answers 422 variant_not_found rather than resolving. A bundle id that names nothing is a 404 instead, so the two halves of the URL are distinguishable.
Required, with no default. How many of this variant ONE bundle contains, never a stock figure and never a number of bundles. An omitted quantity would read as zero, which the column refuses, so it is asked for rather than guessed. A value outside the range is a 400.
1 <= x <= 1000000Response
Success
