{"openapi":"3.0.0","paths":{"/api/v1/webhooks/events":{"get":{"operationId":"OutboundWebhooksController_listEvents","summary":"List the event names available to subscribe to","parameters":[],"responses":{"200":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/webhooks":{"get":{"operationId":"OutboundWebhooksController_list","summary":"List webhook endpoints for the caller org","parameters":[],"responses":{"200":{"description":""}},"tags":["Outbound Webhooks"]},"post":{"operationId":"OutboundWebhooksController_create","summary":"Create a new webhook endpoint (returns secret — store it now)","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEndpointDto"}}}},"responses":{"201":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/webhooks/{id}":{"patch":{"operationId":"OutboundWebhooksController_update","summary":"Update a webhook endpoint","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEndpointDto"}}}},"responses":{"200":{"description":""}},"tags":["Outbound Webhooks"]},"delete":{"operationId":"OutboundWebhooksController_delete","summary":"Delete a webhook endpoint (cascades to its deliveries)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/webhooks/{id}/rotate-secret":{"post":{"operationId":"OutboundWebhooksController_rotateSecret","summary":"Generate a new signing secret (invalidates the old one immediately)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/webhooks/{id}/deliveries":{"get":{"operationId":"OutboundWebhooksController_listDeliveries","summary":"List recent deliveries for an endpoint (newest first)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/webhooks/deliveries/{deliveryId}/replay":{"post":{"operationId":"OutboundWebhooksController_replay","summary":"Re-queue a failed delivery for another attempt","parameters":[{"name":"deliveryId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["Outbound Webhooks"]}},"/api/v1/purchase-orders":{"post":{"operationId":"PurchaseOrdersController_create","summary":"Create a purchase order","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePurchaseOrderDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]},"get":{"operationId":"PurchaseOrdersController_findAll","summary":"List purchase orders (filter by status, channel, search)","parameters":[{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"reconciliation","required":false,"in":"query","description":"Where the order stands with the bookkeeper. NOT_INVOICED → no supplier invoice filed. INVOICED → an invoice exists but the order is not fully checked (an invoice is unreconciled, or a warehouse order has not been fully received). RECONCILED → every invoice reconciled and, for warehouse orders, everything received. Every row also carries this as `reconciliation.status`. Payment is separate — see the invoices' own status.","schema":{"enum":["NOT_INVOICED","INVOICED","RECONCILED"],"type":"string"}},{"name":"awaitingBill","required":false,"in":"query","description":"true → goods have moved but no supplier invoice is filed: a warehouse order that has started receiving, or a dropship order with tracking/shipped/delivered stamped, with reconciliation NOT_INVOICED. The queue for chasing the bill — distinct from hasInvoice=false&hasAttachment=true, which needs a document to have arrived first.","schema":{"type":"string"}},{"name":"hasInvoice","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasAttachment","required":false,"in":"query","description":"true → only POs carrying at least one document.","schema":{"type":"string"}},{"name":"hasUncheckedDocument","required":false,"in":"query","description":"true → only POs carrying a readable document (image or PDF) whose figures nobody has checked yet. This is the needs-attention queue; unreadable attachments are excluded so they cannot pin an order in it forever.","schema":{"type":"string"}},{"name":"hasFailedCheck","required":false,"in":"query","description":"true → only POs where a document was checked and did not come out clean. This is the EXCEPTIONS queue, and it is the other half of hasUncheckedDocument: recording a verdict drops an order out of the needs-checking queue whether it passed or failed, so without this a discrepancy is found once and then never listed again. Use it to work the failures after a triage pass. false → only POs with no failed check on record.","schema":{"type":"string"}},{"name":"hasUnsourcedLine","required":false,"in":"query","description":"true → only POs with a line ordered from a supplier who has no source SKU for it. See GET /purchase-orders/unsourced-lines for what to fix.","schema":{"type":"string"}},{"name":"attachmentType","required":false,"in":"query","description":"Narrow to POs carrying a document of this kind. \"image\" finds hand-captured receipts — inbound vendor mail never attaches images, so every image was uploaded deliberately. Implies hasAttachment=true.","schema":{"enum":["image","pdf"],"type":"string"}},{"name":"startDate","required":false,"in":"query","description":"ISO date-time. Only POs ordered at or after this (orderedAt; createdAt for a draft never sent).","schema":{"type":"string"}},{"name":"endDate","required":false,"in":"query","description":"ISO date-time. Only POs ordered at or before this. With startDate, e.g. one calendar month.","schema":{"type":"string"}},{"name":"active","required":false,"in":"query","description":"true → leave out drafts and cancelled orders (the set the per-month reconciliation counts use). Ignored when `status` is given.","schema":{"type":"string"}},{"name":"held","required":false,"in":"query","description":"true → only orders a person has put on hold for review, with a reason (see POST /purchase-orders/:id/hold); false → none on hold.","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"take","required":true,"in":"query","schema":{"type":"string"}},{"name":"skip","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/trash":{"get":{"operationId":"PurchaseOrdersController_findDeleted","summary":"List soft-deleted (trashed) purchase orders","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/bulk-from-requirements":{"post":{"operationId":"PurchaseOrdersController_bulkFromRequirements","summary":"Bulk-create purchase orders from procurement requirements","parameters":[],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/reconciliation-by-period":{"get":{"operationId":"PurchaseOrdersController_getReconciliationByPeriod","summary":"How many purchase orders are reconciled, per period","description":"Pass `boundaries` as comma-separated ISO date-times, n+1 of them for n periods (e.g. the first instant of each month in your time zone, plus the first of the month after the last). Each period counts the orders placed in it (orderedAt; createdAt for one never sent) by reconciliation status — the same rule as the list's `reconciliation` filter with `startDate`/`endDate` — and adds up what those orders are for (`value`, quantity ordered × unit cost). `total` covers the whole range. Drafts and cancelled orders are not counted. `channelId` limits it to one supplier; `bySupplier=true` adds a `suppliers` breakdown to every period and to `total`, most unreconciled spend first.","parameters":[{"name":"boundaries","required":true,"in":"query","schema":{"type":"string"}},{"name":"channelId","required":false,"in":"query","description":"Only this supplier","schema":{"type":"string"}},{"name":"bySupplier","required":false,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/price-variance":{"get":{"operationId":"PurchaseOrdersController_getPriceVariance","summary":"Billed vs ordered prices, per supplier","description":"For live supplier bills on orders placed between `startDate` and `endDate` (orderedAt; createdAt for one never sent; drafts and cancelled left out): every bill line matched to an order line compares the unit price billed with the unit cost on the purchase order, times the units billed. `variance` > 0 means billed more than ordered. Bill lines matched to no order line, and shipping, tax and other charges, are totalled separately. `suppliers` is biggest overcharge first; `lines` lists the individual price differences, largest first (up to `lineLimit`, default 100, max 500). Range at most 37 months.","parameters":[{"name":"startDate","required":true,"in":"query","schema":{"type":"string"}},{"name":"endDate","required":true,"in":"query","schema":{"type":"string"}},{"name":"channelId","required":false,"in":"query","description":"Only this supplier","schema":{"type":"string"}},{"name":"lineLimit","required":false,"in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/unsourced-lines":{"get":{"operationId":"PurchaseOrdersController_getUnsourcedLines","summary":"Products ordered from a supplier who has no source SKU for them","description":"The fallout of custom purchase order lines. A line with no channelSkuId has nowhere to record a cost, so it cannot be priced by an import, by a PO edit, or by reconciling an invoice, and it will not auto-match to an invoice line. Grouped by variant, so the same product typed several ways collapses to one row and `partNumbers` lists every spelling. Lines with no variant at all are reported separately — those are genuinely free text and a source SKU is not the fix.","parameters":[],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}":{"get":{"operationId":"PurchaseOrdersController_findOne","summary":"Get a purchase order by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]},"put":{"operationId":"PurchaseOrdersController_update","summary":"Update a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePurchaseOrderDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]},"delete":{"operationId":"PurchaseOrdersController_delete","summary":"Soft-delete a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/reconciliation":{"get":{"operationId":"PurchaseOrdersController_getReconciliation","summary":"Compare what was ordered, received and invoiced on a purchase order","description":"The three-way check a bookkeeper does before confirming a supplier bill. Per line: quantity ordered, received and invoiced, the ordered and invoiced unit cost, and the variance. Then the invoices on the order with their charges, document totals, `blockers` (why confirming is not allowed yet — no invoice, a draft/pending/disputed invoice, a warehouse order not fully received, a cancelled order) and `warnings` (variances and unmatched invoice lines, worth a look but never blocking). `canReconcile` is true when POST /purchase-orders/:id/reconcile would succeed. `review` says whether a person has to look before confirming (`needsReview`) and why (`reasons`): material cost changes, low margin or a cost at/above MAP, a first cost, quantity gaps, unmatched lines, sales tax (always), a supplier document whose latest check failed, shipping or fees well above the order (on a dropship order with a known sale: supplier bills across every PO on that sale over 85% of it), an invoice over $2,500, a possible duplicate, or an invoice dated 90+ days from the order. `review.notes` lists payables findings — a payment reported but not confirmed, a bill marked paid with no reference, an unpaid bill with no due date — which are worth saying but never make `needsReview` true: whether a bill was paid is settled in the accounting system, not by reconciling.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/reconcile":{"post":{"operationId":"PurchaseOrdersController_reconcile","summary":"Confirm a purchase order reconciles: mark every invoice on it reconciled","description":"Marks every live invoice on the order reconciled in one step, with everything that already means for a single invoice: each matched line's invoiced unit cost becomes the supplier's cost of record, and the QuickBooks bill push is released for orgs that post on reconcile. Refuses with 400 and the `blockers` list when any invoice is still draft/pending/disputed, a warehouse order is not fully received or its invoice has no line items, no invoice is filed, or the order is a draft or cancelled — nothing is reconciled in that case. When the invoices cover fewer units than were ordered, the same 400 carries `acknowledgements` and the call must be repeated with `{ acceptPartial: true }` to confirm a split shipment on purpose. The response carries `costWriteBack`: which lines updated a supplier cost (from → to), which were skipped and why, and whether `propagation` to product costs is ON or OFF for the org. Idempotent once RECONCILED. With `{ onlyIfClean: true }` it also refuses (400, code NEEDS_REVIEW, with `reasons`) whenever the report's `review.needsReview` is true — for unattended callers that may confirm only what needs no person.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/hold":{"post":{"operationId":"PurchaseOrdersController_hold","summary":"Hold a purchase order for review, saying why","description":"Records why the order is not being reconciled yet (\"customer used a coupon — check the price first\"), who held it and when. While held, the reconciliation review carries a HELD reason, so `reconcile` with `onlyIfClean` refuses it and agents leave it alone. Holding a held order replaces the reason. Refused on an order already reconciled. Released by POST :id/release, or by reconciling the order.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/release":{"post":{"operationId":"PurchaseOrdersController_release","summary":"Take the review hold off a purchase order","description":"Clears the hold without reconciling; `note` (optional) is logged with the reason the order had been held. No-op when the order is not held.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/restore":{"post":{"operationId":"PurchaseOrdersController_restore","summary":"Restore a soft-deleted purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/submit":{"post":{"operationId":"PurchaseOrdersController_submit","summary":"Submit a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/cancel":{"post":{"operationId":"PurchaseOrdersController_cancel","summary":"Cancel a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/change-supplier":{"post":{"operationId":"PurchaseOrdersController_changeSupplier","summary":"Move a purchase order to a different supplier, keeping its number","description":"For an order allocated to the wrong supplier and placed with the right one outside Foundry — the right supplier's invoices quote this order number. `channelId` must be a supplier channel (INBOUND or BOTH) other than the current one. Each line takes the new supplier's SKU for its product when there is exactly one, and none otherwise; dropship reservations on the order move to the new supplier. Sends no email and does not re-submit: status, email history and the vendor link are unchanged. `note` goes on the activity log, not the order notes. Refused on a cancelled order, or once an invoice or bill (not voided) is filed against it or it is reconciled.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeSupplierDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/send-email":{"post":{"operationId":"PurchaseOrdersController_sendEmail","summary":"Email a purchase order to the supplier","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/send-follow-up":{"post":{"operationId":"PurchaseOrdersController_sendFollowUp","summary":"Send a follow-up message on a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/pdf":{"get":{"operationId":"PurchaseOrdersController_downloadPdf","summary":"Download a purchase order as PDF","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/lines":{"post":{"operationId":"PurchaseOrdersController_addLine","summary":"Add a line item to a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/lines/{lineId}":{"put":{"operationId":"PurchaseOrdersController_updateLine","summary":"Update a purchase order line item","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]},"delete":{"operationId":"PurchaseOrdersController_deleteLine","summary":"Delete a purchase order line item","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/lines/{lineId}/receive":{"post":{"operationId":"PurchaseOrdersController_receiveLine","summary":"Receive quantity against a purchase order line","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReceiveLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/close-short":{"post":{"operationId":"PurchaseOrdersController_closeShort","summary":"Close every outstanding warehouse line short: the rest will not ship","description":"Applies close-short to each line with units still owed, in one step and with one history entry. Warehouse orders in SUBMITTED or PARTIAL only. Returns the updated purchase order.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/lines/{lineId}/close-short":{"post":{"operationId":"PurchaseOrdersController_closeLineShort","summary":"Close a warehouse line short: the rest will not ship","description":"Sets the line's ordered quantity to what has been received, so a short-shipped warehouse order can reach RECEIVED and be reconciled against what actually arrived. Warehouse orders in SUBMITTED or PARTIAL only; the original quantity and the reason are written to the purchase order's history. Dropship lines cannot be closed this way — cancel the purchase order instead.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/lines/{lineId}/link-reservations":{"post":{"operationId":"PurchaseOrdersController_linkReservations","summary":"Link order reservations to a purchase order line","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkReservationsDto"}}}},"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/reservations":{"get":{"operationId":"PurchaseOrdersController_getReservations","summary":"List reservations linked to a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/linkable-reservations":{"get":{"operationId":"PurchaseOrdersController_getLinkableReservations","summary":"List reservations that can be linked to a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/messages":{"get":{"operationId":"PurchaseOrdersController_getMessages","summary":"List communication messages for a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/tracking":{"post":{"operationId":"PurchaseOrdersController_addTracking","summary":"Add tracking information to a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddTrackingDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/attachments":{"get":{"operationId":"PurchaseOrdersController_getAttachments","summary":"List attachments on a purchase order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]},"post":{"operationId":"PurchaseOrdersController_createAttachment","summary":"Record a purchase order attachment after upload","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/attachments/{attachmentId}/pages":{"get":{"operationId":"PurchaseOrdersController_getAttachmentPages","summary":"A PDF attachment rendered to page images","description":"MCP tool results have no document content type, and clients decline to render an embedded PDF resource — so a PDF is unreadable to a model unless it is rasterised. Returns presigned JPEGs, one per page, cached after the first call. Images need no rendering; read them from the attachment URL directly.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/attachments/upload-url":{"post":{"operationId":"PurchaseOrdersController_getAttachmentUploadUrl","summary":"Get a presigned URL to upload a purchase order attachment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"filename","required":true,"in":"query","schema":{"type":"string"}},{"name":"contentType","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/attachments/{attachmentId}":{"delete":{"operationId":"PurchaseOrdersController_deleteAttachment","summary":"Delete a purchase order attachment","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/purchase-orders/{id}/documents/check":{"post":{"operationId":"PurchaseOrdersController_checkDocument","summary":"Check figures read off a supplier document against this purchase order","description":"Takes what was read off an invoice, receipt or order confirmation and argues with it, using two things that cost nothing to consult: the document's own arithmetic, and the purchase order we already hold. Returns a verdict per check — PASS, FAIL, or SKIP where the check could not be evaluated — so a figure that does not add up is flagged before anyone files it.\n\nChanges nothing on the order: no invoice is created, no tracking is set. It does record the VERDICT (not the figures) against the document, which is what lets a purchase order leave the needs-checking queue — pass `attachmentId` to name the document, or the check runs and clears nothing.\n\n`safeToPrefillInvoice` is false when any check failed outright; treat that as 'a person must look at this', not as 'the reading was wrong'.\n\nWhether there is anything to FILE is a different question, answered by `paymentStatus` and `dispositionNote`. Do not decide it from the document's title: many suppliers charge the card at order time, so their order confirmation is a receipt and the payable is already settled. Report the payment fields you read — `amountPaid`, `balanceDue`, `paymentMethod`, `paymentReference`, `paidDate` — and let the verdict come from those. Omitting them makes a settled document indistinguishable from an unpaid one.\n\nA document whose lines mostly belong to other orders fails `linesMatchPo`. That is deliberate: a bill hangs off exactly one purchase order. A supplier invoice covering a month of orders is recorded once as a combined bill (POST /combined-bills), which splits it into one bill per order.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDocumentDto"}}}},"responses":{"201":{"description":""}},"tags":["purchase-orders"]}},"/api/v1/invoices/self-bill":{"post":{"operationId":"InvoicesController_selfBill","summary":"Draft bills for suppliers that never send one","description":"For suppliers whose billing profile says they never send a bill (NO_BILL), drafts a bill for every order that has shipped (dropship, tracking recorded) or been received (warehouse) and has no bill yet, at the supplier's price-sheet cost. This happens automatically when tracking is added or goods are received; this catches up orders from before the supplier was set up that way. Drafts always start as DRAFT and need approving. Pass channelId to limit it to one supplier.","parameters":[],"responses":{"201":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices":{"post":{"operationId":"InvoicesController_create","summary":"Create a vendor invoice","description":"Standard invoices (invoiceType INVOICE) require purchaseOrderId; credit/debit memos may reference a related invoice instead. Subtotal and total are computed from the lines and charges when omitted. Initial status depends on the org invoice workflow: OUTSTANDING (simple) or DRAFT (advanced), or PAID when markAsPaid or paidAt is set.\n\nWithout invoices.approve a bill is never filed PAID: any payment details sent (paidAt, markAsPaid, paymentReference, paymentMethod) are kept as reportedPaidAt / reportedPaymentReference / reportedPaymentMethod, the bill is filed unpaid, and the response carries paymentAwaitingApproval. An approver confirms the payment with payment-record or mark-paid, or rejects it with payment-record markUnpaid.\n\ninvoiceDate may be omitted only when sourceAttachmentId is given: supplier paperwork often prints no date, so the invoice is dated from that document’s upload date instead of being refused. The response reports invoiceDateSource as PROVIDED or DOCUMENT_UPLOAD, and the activity log records which document supplied the date — this date drives due-date arithmetic and the aging report, so a fallback is never left looking like a stated one.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvoiceDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]},"get":{"operationId":"InvoicesController_findAll","summary":"List invoices (filter by purchase order, status, type, reconciliation, search)","description":"status and invoiceType accept comma-separated lists; search matches invoice number, PO number, and vendor name. reconciliation=NONE lists invoices nobody has checked against their purchase order yet (whatever their payment status), reconciliation=RECONCILED the checked ones. hasTax=true lists invoices that charged sales tax. paymentAwaitingApproval=true lists unpaid bills carrying a payment reported at filing that no approver has confirmed or rejected yet (it overrides status); each row includes reportedPaidAt, reportedPaymentReference and reportedPaymentMethod.","parameters":[{"name":"purchaseOrderId","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"invoiceType","required":true,"in":"query","schema":{"type":"string"}},{"name":"reconciliation","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasTax","required":true,"in":"query","schema":{"type":"string"}},{"name":"paymentAwaitingApproval","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}},{"name":"take","required":true,"in":"query","schema":{"type":"string"}},{"name":"skip","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/stats":{"get":{"operationId":"InvoicesController_getStats","summary":"Get invoice summary statistics (counts plus outstanding, credit-memo, and debit-memo totals)","parameters":[],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/aging":{"get":{"operationId":"InvoicesController_getAging","summary":"Get an accounts-payable aging report of unpaid invoices bucketed by days past due","parameters":[],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/vendor-statement":{"get":{"operationId":"InvoicesController_getVendorStatement","summary":"Get a vendor statement for a supplier channel","description":"Lists the vendor’s invoices in the optional date range with invoice, credit, debit, paid, and outstanding totals (net = invoices + debits − credits).","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateFrom","required":true,"in":"query","schema":{"type":"string"}},{"name":"dateTo","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/vendor-statement/compare":{"post":{"operationId":"InvoicesController_compareVendorStatement","summary":"Compare a supplier's statement against Foundry's bills","description":"A supplier statement lists the invoices a supplier issued and the payments and credits they received over a period, ending in a closing balance. It is never posted as a bill — every charge on it should already be in the books as its own invoice. This checks the books against it and writes nothing.\n\nSend each statement line as printed: `reference`, `date` (YYYY-MM-DD), `amount` (positive for a charge, negative for a payment or credit). Lines are matched to this supplier's bills in two passes. First by reference: trimmed and ignoring case, against the invoice number, the vendor's order number, or our purchase order number. Then, for lines still unmatched, by the same amount to the cent with the bill dated within 7 days, nearest date first. Each bill is used at most once. Negative lines match only credit memos; a negative line that matches none is reported under `payments`, not as missing, because Foundry records a payment on the bill it settles.\n\nResponse: `matched` (with `difference` = statement − Foundry, so a reference match at the wrong amount is reported, never hidden), `missingInFoundry` (statement lines we hold no bill for, each with a `reason`), `missingOnStatement` (our bills dated in the period that the statement does not mention — `from`..`to`, defaulting to the span of the row dates), `payments`, and `totals`. `totals.foundryOpenBalance` is what we owe the supplier as of `to`: charges not paid by then, less credits drawn against them, less unspent supplier credit (credit memos on a purchase order are excluded — they already reduced that order's cost). `balanceDifference` = `closingBalance` − `foundryOpenBalance`, null when no closing balance is sent.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompareStatementDto"}}}},"responses":{"201":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/supplier-credits":{"get":{"operationId":"InvoicesController_getSupplierCredits","summary":"What a supplier is holding of ours, and which credits make it up","description":"The vendor's credit balance: credit memos filed against the SUPPLIER rather than against one of our purchase orders, less whatever has already been drawn down.\n\nThat distinction is the whole point. A credit memo filed against a purchase order has already reduced that order's cost and is spent. A credit memo with no purchase order is money the vendor owes us — from a return, an overcharge, a rebate, or a coupon code issued in lieu of cash — waiting to be spent on an order nobody has placed yet. Only the second kind appears here, because counting both would let the same money be spent twice.\n\nEach credit reports `remaining` alongside `totalAmount`, and lists where the rest of it went.","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}/apply-credit":{"post":{"operationId":"InvoicesController_applyCredit","summary":"Settle part or all of an invoice with a supplier credit","description":"Spends a vendor credit against this invoice. This is a PAYMENT: the invoice's lines, subtotal and total do not move, only how much of it is still owed. When the credit covers the balance the invoice goes PAID, with the credit named as the payment reference — if the caller holds invoices.approve. Without it the credit is still applied, but the settlement is held as a reported payment (reportedPaymentMethod OTHER, reportedPaymentReference 'Vendor credit …'), the bill stays unpaid, and the response carries paymentAwaitingApproval: saying a bill is paid is an approver's decision, here as at filing. An approver confirms it with payment-record.\n\nUse this for the vendor who hands out a coupon code as a credit note for a previous order. The goods on THIS order cost what they cost; recording them at the discounted checkout total would say these goods were cheap, which is not what happened — they were paid for with money the vendor already owed. Record the invoice at full cost and settle it here.\n\nRefused when the credit belongs to another supplier, when it is already spent, when it would overpay the bill, or when it is a credit filed against a purchase order (that one has already reduced that order's cost).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyCreditDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/apply-credit/{creditMemoId}":{"delete":{"operationId":"InvoicesController_removeCredit","summary":"Undo a credit application","description":"Puts the credit back on the supplier’s balance. If the credit is what settled this invoice, the invoice goes back to OUTSTANDING; an invoice paid some other way stays paid. A settlement by this credit still waiting on an approver (a reported payment naming it) is withdrawn with it.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"creditMemoId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/payment-confirmations":{"post":{"operationId":"InvoicesController_confirmReportedPayments","summary":"Confirm a batch of payments reported at filing","description":"The bulk form of payment-record for the \"Payments to confirm\" queue (GET /invoices?paymentAwaitingApproval=true): an approver checks reported payments against a card or bank statement and confirms them together. Each item records paidAt with the reported payment reference and method unless the item overrides them, and is confirmed through exactly the same checks as payment-record — refused on a reconciled invoice or one already posted to the accounting system.\n\nItems are independent: one refused does not undo the others. An invoice with no reported payment is refused — this confirms reports, it does not mark arbitrary bills paid. `reason` is logged on every invoice. Response: { confirmed: [{invoiceId, label}], failed: [{invoiceId, error}] }.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmReportedPaymentsDto"}}}},"responses":{"201":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/trash":{"get":{"operationId":"InvoicesController_findDeleted","summary":"List soft-deleted (trashed) invoices","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}":{"get":{"operationId":"InvoicesController_findOne","summary":"Get an invoice by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]},"put":{"operationId":"InvoicesController_update","summary":"Update an invoice","description":"Only DRAFT invoices can be updated.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInvoiceDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]},"delete":{"operationId":"InvoicesController_delete","summary":"Soft-delete an invoice","description":"Only DRAFT invoices can be deleted. For a filed invoice that should not be in the books, see POST /invoices/:id/void.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}/revise":{"post":{"operationId":"InvoicesController_revise","summary":"Correct a filed invoice (lines, charges, dates) until it is reconciled","description":"The edit for an invoice that has left DRAFT — in the simple workflow, every filed invoice. Omitted fields are unchanged; `lines`, when sent, replaces every line, so a charge can move to a line in one call. Allowed on DRAFT, OUTSTANDING and PAID invoices that are not reconciled and not yet posted to the accounting system. On a PAID invoice the total may not change: rearrange lines and charges under it, or file a memo for a different amount. `reason` is required and is logged with the before and after.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviseInvoiceDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/restore":{"post":{"operationId":"InvoicesController_restore","summary":"Restore a soft-deleted invoice","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/void":{"post":{"operationId":"InvoicesController_void","summary":"Void a filed invoice (move it to trash with a reason)","description":"For an invoice that should never have been in the books: filed against a purchase order that was then cancelled, or disputed and withdrawn by the supplier. Works from any status except PAID — a paid invoice keeps its record and is corrected with a credit memo. The invoice goes to the trash (restorable via POST /invoices/:id/restore), the purchase order drops back to NOT_INVOICED if it was the only one, and the reason is written on both the invoice and the purchase order.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}/lines":{"post":{"operationId":"InvoicesController_addLine","summary":"Add a line item to an invoice","description":"Only on DRAFT invoices. A purchaseOrderLineId must belong to the invoice’s purchase order; when matched, quantity and cost variances against the PO line are calculated.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddInvoiceLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/lines/{lineId}":{"put":{"operationId":"InvoicesController_updateLine","summary":"Update an invoice line item","description":"Only on DRAFT invoices; the line total and any PO-line variances are recalculated.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInvoiceLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]},"delete":{"operationId":"InvoicesController_deleteLine","summary":"Delete an invoice line item","description":"Only on DRAFT invoices.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}/lines/{lineId}/match":{"post":{"operationId":"InvoicesController_matchLine","summary":"Match an invoice line to a purchase order line","description":"The PO line must belong to the invoice’s purchase order. Recalculates quantity variance (invoiced − received) and cost variance ((invoice cost − PO cost) × invoiced quantity).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/auto-match":{"post":{"operationId":"InvoicesController_autoMatch","summary":"Auto-match unmatched invoice lines to purchase order lines by SKU","description":"Matches each unmatched line whose vendorSku equals any identifier on a PO line — the supplier part number, the supplier channel SKU, or the variant SKU — calculating variances. Returns the number of lines matched.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["invoices"]}},"/api/v1/invoices/{id}/submit":{"post":{"operationId":"InvoicesController_submit","summary":"Submit a DRAFT invoice for approval","description":"Moves the invoice from DRAFT to PENDING.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/approve":{"post":{"operationId":"InvoicesController_approve","summary":"Approve an invoice","description":"Allowed from DRAFT, PENDING, or DISPUTED status.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/dispute":{"post":{"operationId":"InvoicesController_dispute","summary":"Dispute an invoice","description":"Allowed from DRAFT, PENDING, or OUTSTANDING status; the optional reason is appended to the invoice notes.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/reopen":{"post":{"operationId":"InvoicesController_reopen","summary":"Reopen a disputed invoice for editing","description":"Returns a DISPUTED invoice to OUTSTANDING (simple workflow) or DRAFT (advanced workflow).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/reconcile":{"post":{"operationId":"InvoicesController_reconcile","summary":"Mark an invoice reconciled against the vendor’s paperwork","description":"Reconciliation is an overlay separate from invoice status; allowed on OUTSTANDING, APPROVED, or PAID invoices. When the org’s QuickBooks connection posts bills on reconcile, this releases the push.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/unreconcile":{"post":{"operationId":"InvoicesController_unreconcile","summary":"Clear the reconciled mark on an invoice","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/mark-paid":{"post":{"operationId":"InvoicesController_markPaid","summary":"Mark an invoice as paid","description":"Only APPROVED or OUTSTANDING invoices can be marked paid; records payment date, reference, and method.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkPaidDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/payment-record":{"post":{"operationId":"InvoicesController_correctPayment","summary":"Correct an invoice's payment record","description":"Write on how a bill was paid (paidAt, paymentReference, paymentMethod), or take a bill marked paid with no evidence back to unpaid (markUnpaid). On an unpaid bill carrying a payment reported at filing, recording paidAt confirms it and markUnpaid rejects it; either clears the report. The amount never changes. Refused on a reconciled invoice or one already posted to the accounting system. Gated like mark-paid.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CorrectPaymentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/invoices/{id}/invoice-number":{"post":{"operationId":"InvoicesController_setInvoiceNumber","summary":"Write in the vendor's invoice number after the fact, or correct a wrong one","description":"For an invoice filed from a document that carried no number — a supplier who charges at order time sends a receipt with an order reference and no invoice number. When their real invoice arrives later, this records it.\n\nWorks at any status, unlike a normal edit, because by then the invoice has long since left DRAFT. It also REPLACES a number that is wrong — an order reference keyed as the invoice number, a typo — when `reason` is sent; the old number and the reason are logged. A replacement is refused once the invoice is reconciled (unreconcile first) or posted to the accounting system (QuickBooks files the bill under the old number — correct it there). The number must be unused in this organization. Never put an order or confirmation reference here; that is vendorOrderNumber.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetInvoiceNumberDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["invoices"]}},"/api/v1/purchase-orders/{poId}/landed-costs":{"post":{"operationId":"PurchaseOrderLandedCostsController_create","summary":"Add a landed cost to a purchase order","description":"Allocation method defaults to BY_VALUE; an invoiceId must belong to the same purchase order.","parameters":[{"name":"poId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLandedCostDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["landed-costs"]},"get":{"operationId":"PurchaseOrderLandedCostsController_findAll","summary":"List landed costs on a purchase order","parameters":[{"name":"poId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["landed-costs"]}},"/api/v1/purchase-orders/{poId}/landed-costs/summary":{"get":{"operationId":"PurchaseOrderLandedCostsController_getCostSummary","summary":"Get a landed-cost summary for a purchase order","description":"Per-line and total breakdown of base (received) cost, allocated landed costs, and invoice variances, with per-unit costs.","parameters":[{"name":"poId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["landed-costs"]}},"/api/v1/landed-costs/{id}":{"get":{"operationId":"LandedCostsController_findOne","summary":"Get a landed cost by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["landed-costs"]},"put":{"operationId":"LandedCostsController_update","summary":"Update a landed cost","description":"Changing the amount or allocation method re-runs allocation unless the method is MANUAL.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLandedCostDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["landed-costs"]},"delete":{"operationId":"LandedCostsController_delete","summary":"Delete a landed cost and its allocations","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["landed-costs"]}},"/api/v1/landed-costs/{id}/allocate":{"post":{"operationId":"LandedCostsController_allocate","summary":"Allocate a landed cost across purchase order lines","description":"Replaces existing allocations, apportioning the amount by received line value (BY_VALUE), received quantity (BY_QUANTITY), or received weight (BY_WEIGHT). MANUAL landed costs must use the allocations endpoint instead.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["landed-costs"]}},"/api/v1/landed-costs/{id}/allocations":{"put":{"operationId":"LandedCostsController_setAllocations","summary":"Set manual per-line allocations for a landed cost","description":"Replaces existing allocations and sets the allocation method to MANUAL; amounts must sum to the landed cost amount (within $0.01).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetManualAllocationsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["landed-costs"]}},"/api/v1/combined-bills":{"get":{"operationId":"CombinedBillsController_list","summary":"List combined bills (supplier invoices covering several purchase orders)","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["combined-bills"]},"post":{"operationId":"CombinedBillsController_create","summary":"Record a supplier invoice that covers several purchase orders","description":"Record it once, as the supplier printed it: goods lines (SKU × quantity × unit cost) and charge lines (shipping, return labels, fees). It starts as a DRAFT; nothing is billed until it is split across orders and posted. Use this instead of filing a monthly invoice or statement against one purchase order.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCombinedBillDto"}}}},"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}":{"get":{"operationId":"CombinedBillsController_findOne","summary":"Get a combined bill with its lines, how each is split across orders, and the per-order bills","description":"`problems` lists what stands between a DRAFT and posting (lines that do not add up to the total, goods or charges not fully placed). `readyToPost` is true when there are none.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["combined-bills"]},"delete":{"operationId":"CombinedBillsController_remove","summary":"Delete a DRAFT combined bill","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/lines":{"put":{"operationId":"CombinedBillsController_replaceLines","summary":"Replace the lines of a DRAFT combined bill (clears its split)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplaceCombinedBillLinesDto"}}}},"responses":{"200":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/suggest":{"post":{"operationId":"CombinedBillsController_suggest","summary":"Suggest where each goods line lands","description":"Looks at the supplier's open purchase order lines in the bill's period (or the 62 days before the invoice date) and places each goods line on lines carrying the SAME SKU, oldest order first. Exact match only — a SKU that differs between the invoice and our orders is reported in `unallocated` as a data-quality finding, never matched loosely. With `apply: true` the suggestion replaces the current goods split and charges are re-split.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/allocations":{"put":{"operationId":"CombinedBillsController_setAllocations","summary":"Set how the bill is split across orders","description":"Goods are placed on purchase order lines by quantity (more than was ordered is allowed; the per-order bill then shows the quantity variance and review flags it). Charges are split across the orders the bill covers BY_VALUE, BY_QUANTITY, EQUAL, or MANUAL with your own amounts — a MANUAL charge may also go on any open order from the supplier that has no goods on the bill (a return label for an order whose goods were never billed); that order then gets a charge-only bill.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetAllocationsDto"}}}},"responses":{"200":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/post":{"post":{"operationId":"CombinedBillsController_post","summary":"Create one bill per purchase order from the combined bill","description":"Refused until every line is placed and the lines add up to the printed total. Each per-order bill is an ordinary bill: it is matched, reviewed and reconciled like any other, and carries the supplier's invoice number as its vendor reference. Shipping on a warehouse order also becomes landed cost. A payment recorded on the combined bill is copied to each bill as a REPORTED payment; an approver confirms it with mark-paid.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/unpost":{"post":{"operationId":"CombinedBillsController_unpost","summary":"Void the per-order bills and return the combined bill to DRAFT","description":"Refused when any per-order bill is reconciled, paid, or already in the accounting system.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnpostCombinedBillDto"}}}},"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/approve":{"post":{"operationId":"CombinedBillsController_approve","summary":"Approve every per-order bill from this invoice in one step","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/mark-paid":{"post":{"operationId":"CombinedBillsController_markPaid","summary":"Mark every per-order bill from this invoice paid","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkCombinedBillPaidDto"}}}},"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/document/upload-url":{"post":{"operationId":"CombinedBillsController_documentUploadUrl","summary":"Get a presigned URL to upload the supplier's document (the invoice PDF) for a combined bill","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"filename","required":true,"in":"query","schema":{"type":"string"}},{"name":"contentType","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["combined-bills"]}},"/api/v1/combined-bills/{id}/document":{"put":{"operationId":"CombinedBillsController_setDocument","summary":"Record the supplier's document on a combined bill after upload","description":"Pass the s3Key returned by document/upload-url. Once the combined bill is posted with its document, its per-order bills count as standing on checked supplier paperwork in the reconciliation review: posting already proved the lines add up to the printed total and every unit is placed.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["combined-bills"]},"delete":{"operationId":"CombinedBillsController_removeDocument","summary":"Remove a combined bill's document","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["combined-bills"]}},"/api/v1/orders":{"get":{"operationId":"OrdersController_findAll","summary":"List orders (filter by channel, status, date range, search)","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"startDate","required":true,"in":"query","schema":{"type":"string"}},{"name":"endDate","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}},{"name":"blockedOnBuild","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/trash":{"get":{"operationId":"OrdersController_findDeleted","summary":"List soft-deleted (trashed) orders","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/stats":{"get":{"operationId":"OrdersController_getStats","summary":"Get order statistics (optionally per channel)","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/pending-reservations":{"get":{"operationId":"OrdersController_getPendingReservations","summary":"List pending reservations","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["orders"]}},"/api/v1/orders/{id}":{"get":{"operationId":"OrdersController_findOne","summary":"Get an order by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/history":{"get":{"operationId":"OrdersController_getStatusHistory","summary":"Get an order's status history","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/reservations":{"get":{"operationId":"OrdersController_getOrderReservations","summary":"List reservations for an order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/reservations/status":{"get":{"operationId":"OrdersController_getReservationStatus","summary":"Whether every hold this order should have exists yet","description":"Compares each line (kits flattened to leaf components) against the holds on record. `complete: false` means reservation writing has not finished — or failed part-way — and a pick list printed now would be missing items.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/reservations/expand-kits":{"post":{"operationId":"OrdersController_expandKitHolds","summary":"Re-expand kit-level holds on an order into component holds","description":"Orders written before bundle expansion went recursive can hold reservations against the kit SKU itself — a virtual variant that owns no stock, so fulfilling moves nothing and the pick list names a SKU on no shelf. This replaces each open kit-level hold with the leaf-component holds the current checkout would create. Idempotent; holds that are shipped, PO-linked, or on kits with no components are left alone and reported.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/lines/{lineId}/refund":{"put":{"operationId":"OrdersController_setLineRefund","summary":"Say how many units of a line were refunded","description":"For a refund the channel recorded as a plain amount (or against the wrong item): `quantity` is the refunded units on this line, `null` goes back to the channel's own figure. Overrides the channel while set. Resizes the line's stock holds to the units still owed — refunded units that have not shipped stop holding stock — except on a finished order or where holds are committed or on a purchase order, which the response says. `reason` is required and logged. Refunded units also leave the order's goods cost.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/lines/{lineId}/refresh-holds":{"post":{"operationId":"OrdersController_refreshLineHolds","summary":"Rebuild one line's stock holds from the current catalog item","description":"Re-flattens the line against its catalog item as it stands today and reconciles the stock holds to match. Use after fixing an item mid-fulfilment — a SIMPLE corrected to a BUNDLE, a kit whose components changed — so the pick list pulls the right things. Refuses lines with committed (already fulfilled) holds or holds linked to a purchase order, and finished orders. Pass `relink: true` to first re-match the line to a catalog item from its channel SKU — for a line whose SKU the channel changed while Foundry kept the old link.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshLineHoldsDto"}}}},"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/release-stale-holds":{"post":{"operationId":"OrdersController_releaseStaleHolds","summary":"Settle open stock holds left behind on orders that already finished","description":"A hold should end when its order does, but orders fulfilled before hold commitment was wired up (or whose webhook was missed) left holds open forever — each one inflating its variant’s reserved total and understating sellable stock on every channel. Each hold gets the remedy it needs, reported per hold as `remedy`: COMMIT when the order shipped AND some of its other holds are already committed, which means a fulfilment pass ran and skipped this one, so the goods left and the stock must come down (a real SALE movement against the order); RELEASE otherwise, because nothing proves those units ever shipped and deducting on a guess invents shipments. Dry run unless dryRun is explicitly false; the dry run returns exactly what would happen, including the toCommit / toRelease split.","parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"dryRun":{"type":"boolean","default":true,"description":"Report what would happen without writing anything; pass false explicitly to settle","example":true},"limit":{"type":"number","default":500,"description":"Maximum holds to examine in one run","example":500}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["orders"]}},"/api/v1/orders/reresolve-lines":{"post":{"operationId":"OrdersController_reresolveLines","summary":"Re-match order lines that were never linked to a variant","description":"Line matching only ran at order-creation time, so a line orphaned by a missing channel listing held no stock and triggered no build forever. This re-runs it. Dry run unless dryRun is explicitly false. Never moves stock: reservations are created only for orders that can still be fulfilled, and already-shipped orders are linked and reported for a human to decide on.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReresolveLinesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReresolveLinesResultDto"}}}},"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/bulk/status":{"put":{"operationId":"OrdersController_bulkUpdateStatus","summary":"Bulk-update status for multiple orders","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkUpdateOrderStatusDto"}}}},"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/status":{"put":{"operationId":"OrdersController_updateStatus","summary":"Update an order's status","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrderStatusDto"}}}},"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/notes":{"put":{"operationId":"OrdersController_updateNotes","summary":"Update an order's notes","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["notes"],"properties":{"notes":{"type":"string","description":"Replacement notes text for the order","example":"Customer asked for signature on delivery"}}}}}},"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/shipping-address":{"put":{"operationId":"OrdersController_updateShippingAddress","summary":"Update an order's shipping address","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["shippingAddress"],"properties":{"shippingAddress":{"type":"object","additionalProperties":true,"description":"Replacement shipping address, stored as given (free-form JSON — the shape the order's sales channel supplied)","example":{"firstName":"Jane","lastName":"Doe","street1":"123 Main St","city":"Portland","state":"OR","zip":"97201","country":"US"}}}}}}},"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/restore":{"post":{"operationId":"OrdersController_restore","summary":"Restore a soft-deleted order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/fulfill":{"post":{"operationId":"OrdersController_fulfill","summary":"Mark an order fulfilled and deduct its stock","description":"For fulfillment that does not come back through a channel or ShipStation — a counter collection, or delivering it yourself. Deducts stock for every open reservation on the order (dropship lines excepted: those never sat in your warehouse). Safe to call twice — a reservation already fulfilled is skipped.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FulfillOrderDto"}}}},"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/restore-and-fulfill-holds":{"post":{"operationId":"OrdersController_restoreAndFulfillHolds","summary":"Restore holds that were released on a shipped order, then fulfil them","description":"Recovery for holds cancelled by a stale-hold sweep on an order that had actually shipped — the units left, nothing was deducted, and no open hold remains to fulfil from. Puts the named holds back exactly as they were and runs them through normal fulfilment, so the stock comes down as a real SALE movement attributed to this order. You must name the reservation ids: a line the customer genuinely removed is also a cancelled hold on a shipped order, and deducting for it would invent a shipment. Holds that are not CANCELLED — a COMMITTED one above all — are skipped and reported, never deducted twice.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reservationIds"],"properties":{"reservationIds":{"type":"array","items":{"type":"string"},"description":"Ids of the CANCELLED holds on this order to restore and fulfil"}}}}}},"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/{id}/sync":{"post":{"operationId":"OrdersController_syncFromChannel","summary":"Re-pull an order from its sales channel","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["orders"]}},"/api/v1/orders/by-status":{"delete":{"operationId":"OrdersController_deleteByStatus","summary":"Delete orders by channel and status (dry-run supported)","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":true,"in":"query","schema":{"type":"string"}},{"name":"dryRun","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["orders"]}},"/api/v1/reservations/{id}/allocate":{"post":{"operationId":"ReservationsController_allocateReservation","summary":"Allocate a reservation to a source channel","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sourceChannelId"],"properties":{"sourceChannelId":{"type":"string","description":"Source (inbound) channel to allocate the reservation to — allowed while the reservation is PENDING or ALLOCATED","example":"5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b"}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["reservations"]}},"/api/v1/reservations/{id}/deallocate":{"post":{"operationId":"ReservationsController_deallocateReservation","summary":"Reverse a reservation allocation (return to PENDING)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["reservations"]}},"/api/v1/reservations/{id}/available-sources":{"get":{"operationId":"ReservationsController_getAvailableSources","summary":"List available source channels for a reservation","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["reservations"]}},"/api/v1/stock/adjust":{"post":{"operationId":"StockController_adjustStock","summary":"Adjust on-hand stock for a variant at a warehouse (with movement reason)","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdjustStockDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["stock"]}},"/api/v1/stock/movements":{"get":{"operationId":"StockController_getMovements","summary":"List stock movements (filter by variant, warehouse, type)","parameters":[{"name":"variantId","required":true,"in":"query","schema":{"type":"string"}},{"name":"warehouseId","required":true,"in":"query","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["stock"]}},"/api/v1/stock/variant/{variantId}/breakdown":{"get":{"operationId":"StockController_getVariantBreakdown","summary":"Per-warehouse stock breakdown for a variant","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["stock"]}},"/api/v1/stock/items/{variantId}/{warehouseId}":{"patch":{"operationId":"StockController_updateItem","summary":"Update a stock item (bin location, reorder point, etc.)","description":"Edits min/max quantities on one stock location. Does not change on-hand quantity or relocate stock — use /stock/adjust or /stock/move for that.","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}},{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"binId":{"type":"string","nullable":true,"description":"Which bin location to edit (null or omitted = the Unassigned location). Selects the row; it is not changed.","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"},"minQuantity":{"type":"number","description":"Reorder point — minimum quantity to keep at this location","example":5},"maxQuantity":{"type":"number","description":"Maximum quantity to stock at this location","example":50}}}}}},"responses":{"200":{"description":""}},"tags":["stock"]},"delete":{"operationId":"StockController_removeItem","summary":"Remove an empty location","description":"The SKU no longer lives in that bin. Refused while anything is on hand, reserved or inbound there — move or adjust it first. The location disappears from every list; its movement history is kept, and if stock lands in that bin again the location reappears. If it was the primary bin, the fullest bin still holding stock takes over.","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}},{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}},{"name":"binId","required":false,"in":"query","description":"Which location to remove (omit for the Unassigned location)","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["stock"]}},"/api/v1/stock/transfer":{"post":{"operationId":"StockController_transfer","summary":"Transfer stock between warehouses","description":"Records a TRANSFER_OUT movement at the source and TRANSFER_IN at the destination. The source bin is resolved automatically (primary bin, else the single bin, else the fullest); fails if the resolved location holds less than the requested quantity.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["variantId","fromWarehouseId","toWarehouseId","quantity"],"properties":{"variantId":{"type":"string","description":"Variant to transfer","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"fromWarehouseId":{"type":"string","description":"Source warehouse","example":"7b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"},"toWarehouseId":{"type":"string","description":"Destination warehouse (must differ from the source)","example":"9d0e1f2a-3b4c-5d6e-7f8a-9b0c1d2e3f4a"},"quantity":{"type":"number","description":"Units to transfer (positive)","example":10},"notes":{"type":"string","description":"Free-text note appended to both movement reasons","example":"Rebalancing ahead of the holiday rush"}}}}}},"responses":{"201":{"description":""}},"tags":["stock"]}},"/api/v1/stock/move":{"post":{"operationId":"StockController_move","summary":"Move stock to a different bin location within a warehouse","description":"Put-away: relocates units between bins in one warehouse. Net-zero across the warehouse, so available quantity is unchanged; a TRANSFER_OUT/TRANSFER_IN movement pair records the relocation.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["variantId","warehouseId","fromBinId","toBinId","quantity"],"properties":{"variantId":{"type":"string","description":"Variant to move","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"warehouseId":{"type":"string","description":"Warehouse the move happens within","example":"7b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"},"fromBinId":{"type":"string","nullable":true,"description":"Bin to move from (null = the Unassigned location)","example":null},"toBinId":{"type":"string","nullable":true,"description":"Bin to move to (null = the Unassigned location; must differ from fromBinId)","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"},"quantity":{"type":"number","description":"Units to move (positive; must not exceed on-hand at the source location)","example":4}}}}}},"responses":{"201":{"description":""}},"tags":["stock"]}},"/api/v1/stock/replenishment":{"get":{"operationId":"StockController_getReplenishment","summary":"Pick faces that need topping up from back stock, in walk order","description":"One task per SKU whose primary bin is below its minimum, below what open orders need, or empty, while another location in the same warehouse holds stock. Nothing is built; stock only changes bins. Act on a task with POST /stock/move from a listed source to the primary bin.","parameters":[{"name":"warehouseId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["stock"]}},"/api/v1/stock/primary-bin":{"post":{"operationId":"StockController_setPrimaryBin","summary":"Designate the bin a SKU is normally pulled from","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPrimaryBinDto"}}}},"responses":{"201":{"description":""}},"tags":["stock"]}},"/api/v1/stock/relocate":{"post":{"operationId":"StockController_relocate","summary":"Change the bin a SKU lives in, with or without stock","description":"Moves the location itself, not just units: works on an empty row, so a SKU can be given a bin before it has stock or moved to a new one after it sells out. Any on-hand stock moves along (recorded as a bin move). The primary flag and replenishment levels follow when the source was the primary bin, or when the SKU had none. The old location disappears from every list; its movement history is kept. Refused while the source has reserved or inbound units.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["variantId","warehouseId","fromBinId","toBinId"],"properties":{"variantId":{"type":"string","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"warehouseId":{"type":"string","example":"7b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"},"fromBinId":{"type":"string","nullable":true,"description":"Bin the SKU lives in now (null = the Unassigned location, which need not exist yet)","example":null},"toBinId":{"type":"string","nullable":true,"description":"Bin it should live in (null = Unassigned; must differ from fromBinId)","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"}}}}}},"responses":{"201":{"description":""}},"tags":["stock"]}},"/api/v1/warehouses/{warehouseId}/stock":{"get":{"operationId":"WarehouseStockController_getWarehouseStock","summary":"List stock items in a warehouse (searchable, paginated)","description":"One row per location a SKU lives in. By default a location with nothing in it (no on-hand, reserved or inbound units) is listed only when it is the SKU's primary bin; pass includeEmpty=true to list every empty location too. An empty Unassigned row beside a real bin is never listed.","parameters":[{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"includeEmpty","required":false,"in":"query","description":"true = also list empty non-primary locations (default false)","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["stock"]}},"/api/v1/warehouses/{warehouseId}/stock/movements":{"get":{"operationId":"WarehouseStockController_getWarehouseMovements","summary":"List stock movements for a warehouse","parameters":[{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["stock"]}},"/api/v1/warehouses/{warehouseId}/stock/export-template":{"get":{"operationId":"WarehouseStockController_exportTemplate","summary":"Download a CSV stock-count template for this warehouse","parameters":[{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}},{"name":"scope","required":true,"in":"query","schema":{"type":"string"}},{"name":"includeEmpty","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["stock"]}},"/api/v1/warehouses/{warehouseId}/stock/count/preview":{"post":{"operationId":"WarehouseStockController_previewCount","summary":"Preview a bulk stock count (diff against current on-hand)","parameters":[{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCountPreviewDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["stock"]}},"/api/v1/warehouses/{warehouseId}/stock/count/apply":{"post":{"operationId":"WarehouseStockController_applyCount","summary":"Apply a bulk stock count","parameters":[{"name":"warehouseId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCountApplyDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["stock"]}},"/api/v1/products":{"get":{"operationId":"ProductsController_findAll","summary":"List products (filter by brand, category, channel, type, search, matched/enriched/archived/hasMap)","description":"Each row carries `customFields`, the legacy free-form JSON blob, which is NOT where typed custom fields live. Pass `include=customValues` to attach each product's own typed values (`customValues: [{fieldDefId, fieldKey, fieldLabel, fieldType, value}]`). Variant-level values: GET /variants?include=customValues.","parameters":[{"name":"include","required":false,"in":"query","description":"Comma-separated extras to attach to every row. Supported: `customValues`.","schema":{"type":"string"}},{"name":"brandId","required":true,"in":"query","schema":{"type":"string"}},{"name":"categoryId","required":true,"in":"query","schema":{"type":"string"}},{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"matched","required":true,"in":"query","schema":{"type":"string"}},{"name":"enriched","required":true,"in":"query","schema":{"type":"string"}},{"name":"archived","required":true,"in":"query","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}},{"name":"marginBelow","required":true,"in":"query","schema":{"type":"string"}},{"name":"marginUnknown","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasMap","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["products"]},"post":{"operationId":"ProductsController_create","summary":"Create a product","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["products"]}},"/api/v1/products/{id}/where-used":{"get":{"operationId":"ProductsController_whereUsed","summary":"List assemblies and bundles that use this product as a component","description":"Where this product is used as a component: every assembly (BOM) and bundle\nthat includes any of the product's variants.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/trash":{"get":{"operationId":"ProductsController_findDeleted","summary":"List soft-deleted (trashed) products","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/data-health":{"get":{"operationId":"ProductsController_dataHealth","summary":"Get catalog data-health report for the org","parameters":[],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/family-candidates":{"get":{"operationId":"ProductsController_familyCandidates","summary":"Detect SIMPLE products that are channel siblings but not family-linked","description":"Detect groups of Foundry SIMPLE products that are siblings on an outbound\nchannel but aren't family-linked in Foundry. Used by the \"Family\nCandidates\" tool to consolidate stranded siblings.","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["products"]}},"/api/v1/products/{id}":{"get":{"operationId":"ProductsController_findOne","summary":"Get a product by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["products"]},"put":{"operationId":"ProductsController_update","summary":"Update a product","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["products"]},"delete":{"operationId":"ProductsController_delete","summary":"Soft-delete a product","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/template-status":{"get":{"operationId":"ProductsController_templateStatus","summary":"Get a product's PIM template status","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["products"]}},"/api/v1/products/{id}/duplicate":{"post":{"operationId":"ProductsController_duplicate","summary":"Duplicate a product","description":"Copies a SIMPLE, BUNDLE or ASSEMBLY product — descriptive fields, brand/category/class, custom fields, images, price/cost/dimensions, the active BOM and bundle components — into a new product with a fresh Foundry SKU. SKU is blank unless supplied; UPC/GTIN/MPN, stock, listings and enrichment are not copied. Families are refused.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DuplicateProductDto"}}}},"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/variant-parent":{"post":{"operationId":"ProductsController_createVariantParent","summary":"Create a FAMILY parent product from existing child products","description":"Create a variant parent product from existing products.\nThe specified child products will be linked to the new parent.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVariantParentDto"}}}},"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/children":{"post":{"operationId":"ProductsController_linkChildren","summary":"Link products as children to a parent product","description":"Link products as children to a parent product.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkChildrenDto"}}}},"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/children/{childId}":{"delete":{"operationId":"ProductsController_unlinkChild","summary":"Unlink a child product from its parent","description":"Unlink a child product from its parent.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"childId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/children/order":{"put":{"operationId":"ProductsController_reorderChildren","summary":"Reorder children under a FAMILY parent","description":"Reorder children under a FAMILY parent. Body: { orderedChildIds: [...] }.\nEach child's `position` is set to its index in the array; children not\nin the list retain their existing position.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderChildrenDto"}}}},"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/convert/preflight":{"get":{"operationId":"ProductsController_convertPreflight","summary":"Preflight check before changing a product's type","description":"Preflight check for changing a product's type. Returns blockers\n(hard-fail) and warnings (acknowledge-required) so the UI can render\na confirmation dialog before calling POST /:id/convert.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"targetType","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["products"]}},"/api/v1/products/{id}/convert":{"post":{"operationId":"ProductsController_convert","summary":"Change a product's type (e.g. SIMPLE→FAMILY/BUNDLE/ASSEMBLY)","description":"Change a product's type. Phase 1 supports SIMPLE→FAMILY,\nSIMPLE→BUNDLE, SIMPLE→ASSEMBLY, BUNDLE→ASSEMBLY. Pure type flip plus\n(for BUNDLE→ASSEMBLY) a BOM bootstrapped from existing bundle rows.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertProductDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["products"]}},"/api/v1/products/bulk-from-source":{"post":{"operationId":"ProductsController_bulkCreateFromSource","summary":"Bulk-create products and variants from source channel SKUs","description":"Bulk create products from source channel SKUs.\nCreates products and variants, linking source SKUs in the process.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkCreateFromSourceDto"}}}},"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/bulk-restore":{"post":{"operationId":"ProductsController_bulkRestore","summary":"Bulk-restore soft-deleted products (by ids, all, or date range)","description":"Bulk restore soft-deleted products.\n- { ids: [...] } restores specific products\n- { all: true } restores every product in the trash\n- { after, before } restores products deleted within a date range","parameters":[],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/restore":{"post":{"operationId":"ProductsController_restore","summary":"Restore a soft-deleted product","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/archive":{"post":{"operationId":"ProductsController_archive","summary":"Archive a product","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/{id}/unarchive":{"post":{"operationId":"ProductsController_unarchive","summary":"Unarchive a product","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["products"]}},"/api/v1/products/bulk":{"delete":{"operationId":"ProductsController_bulkDelete","summary":"Bulk soft-delete products (by ids, or all with confirm)","description":"Bulk delete products (soft-delete)\n- Pass { ids: ['id1', 'id2'] } to delete specific products\n- Pass { all: true, confirm: \"DELETE_ALL_PRODUCTS\" } to delete all products","parameters":[],"responses":{"200":{"description":""}},"tags":["products"]}},"/api/v1/variants":{"get":{"operationId":"VariantsController_findAll","summary":"List variants (filter by product, option type, search)","description":"Each row carries `customFields`, the legacy free-form JSON blob, which is NOT where typed custom fields live. Pass `include=customValues` to attach each variant's typed values (`customValues: [{fieldDefId, fieldKey, fieldLabel, fieldType, value}]`) — the same values GET /variants/{id}/custom-values returns — instead of fetching them one variant at a time.","parameters":[{"name":"productId","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"optionType","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"include","required":false,"in":"query","description":"Comma-separated extras to attach to every row. Supported: `customValues`.","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]},"post":{"operationId":"VariantsController_create","summary":"Create a variant","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateVariantDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]}},"/api/v1/variants/duplicates":{"get":{"operationId":"VariantsController_detectDuplicates","summary":"Detect duplicate variants","parameters":[{"name":"matchBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/sourcing-coverage":{"get":{"operationId":"VariantsController_sourcingCoverage","summary":"Sourcing coverage, resolved through bundle/assembly components","description":"Sourcing coverage resolved THROUGH bundle components and BOM lines.\n\nPrefer this over `needing-suppliers` (and the `hasSource` channel-SKU\nfilter) for anything involving kits: both of those ask only whether the\nvariant itself carries a supplier row, so every BUNDLE and ASSEMBLY reads\nas unsourced no matter how well sourced its components are — the count of\n\"unsourced\" products goes UP as you convert families to kits.\n\n`gaps` is the work list: a kit never needs a supplier of its own, so every\nfix lands on a leaf part, and one leaf can unblock many products.","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"variantIds","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]}},"/api/v1/variants/needing-suppliers":{"get":{"operationId":"VariantsController_findNeedingSuppliers","summary":"List variants that have no linked supplier","description":"Variants that cannot be traced to anything purchasable.\n\nStructure-aware since the stored `sourcingStatus` landed: a fully-sourced\nBUNDLE or ASSEMBLY is no longer listed here just for holding no supplier row\nof its own. Use `sourcing-coverage` when you want the gap leaves and what\neach one blocks rather than a flat list.","parameters":[{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/export":{"get":{"operationId":"VariantsController_exportCsv","summary":"Export variants to CSV (by ids or filters)","description":"Export variants to CSV.\nGET /variants/export?ids=a,b,c        (multi-select export)\nGET /variants/export?productId=xxx    (filter export)\nWhen `ids` is provided, all other filter params are ignored.","parameters":[{"name":"ids","required":true,"in":"query","schema":{"type":"string"}},{"name":"productIds","required":true,"in":"query","schema":{"type":"string"}},{"name":"productId","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"type","required":true,"in":"query","schema":{"type":"string"}},{"name":"brandId","required":true,"in":"query","schema":{"type":"string"}},{"name":"matched","required":true,"in":"query","schema":{"type":"string"}},{"name":"enriched","required":true,"in":"query","schema":{"type":"string"}},{"name":"productStatus","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/import":{"post":{"operationId":"VariantsController_importCsv","summary":"Import variants from an uploaded CSV file","parameters":[],"responses":{"201":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}":{"get":{"operationId":"VariantsController_findOne","summary":"Get a variant by id","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]},"put":{"operationId":"VariantsController_update","summary":"Update a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateVariantDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]},"delete":{"operationId":"VariantsController_delete","summary":"Soft-delete a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/where-used":{"get":{"operationId":"VariantsController_whereUsed","summary":"List BOMs where this variant is used as a component","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/supplier-suggestions":{"get":{"operationId":"VariantsController_getSupplierSuggestions","summary":"Get supplier suggestions for a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/restore":{"post":{"operationId":"VariantsController_restore","summary":"Restore a soft-deleted variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/use-source-cost":{"post":{"operationId":"VariantsController_useSourceCost","summary":"Clear the manual-cost lock and re-resolve cost from sources","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/use-source-map":{"post":{"operationId":"VariantsController_useSourceMap","summary":"Clear the manual-MAP lock and re-resolve MAP from sources","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["variants"]}},"/api/v1/variants/aggregate-inventory":{"post":{"operationId":"VariantsController_aggregateInventory","summary":"Trigger inventory aggregation across all variants","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]}},"/api/v1/variants/{id}/sources":{"get":{"operationId":"VariantsController_getAvailableSources","summary":"List available sourcing options for a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/upload-url":{"get":{"operationId":"VariantsController_getImageUploadUrl","summary":"Get a presigned S3 URL to upload a variant image","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"filename","required":true,"in":"query","schema":{"type":"string"}},{"name":"contentType","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/images":{"post":{"operationId":"VariantsController_addImage","summary":"Add an image to a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]},"delete":{"operationId":"VariantsController_removeImage","summary":"Remove an image from a variant","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]}},"/api/v1/variants/{id}/images/from-url":{"post":{"operationId":"VariantsController_addImagesFromUrl","summary":"Attach image(s) to a variant by URL, re-hosting external URLs into S3","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["variants"]}},"/api/v1/variants/{id}/images/reorder":{"put":{"operationId":"VariantsController_reorderImages","summary":"Reorder a variant's images","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["variants"]}},"/api/v1/bundles/export":{"get":{"operationId":"BundlesController_exportBundles","summary":"Export bundle component lists to CSV (optionally by bundle variant ids)","parameters":[{"name":"variantIds","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["bundles"]}},"/api/v1/bundles/import":{"post":{"operationId":"BundlesController_importBundles","summary":"Import bundle component lists from an uploaded CSV file","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["bundles"]}},"/api/v1/bundles/{bundleVariantId}/components":{"get":{"operationId":"BundlesController_getComponents","summary":"List a bundle's components with each component variant's stock and cost figures","parameters":[{"name":"bundleVariantId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["bundles"]},"post":{"operationId":"BundlesController_addComponent","summary":"Add a component variant to a bundle","description":"Upserts: adding a variant already in the bundle overwrites its quantity. Self-reference and transitive component cycles are rejected.","parameters":[{"name":"bundleVariantId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddComponentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["bundles"]}},"/api/v1/bundles/{bundleVariantId}/components/{componentVariantId}":{"put":{"operationId":"BundlesController_updateQuantity","summary":"Update a bundle component's per-bundle quantity","parameters":[{"name":"bundleVariantId","required":true,"in":"path","schema":{"type":"string"}},{"name":"componentVariantId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuantityDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["bundles"]},"delete":{"operationId":"BundlesController_removeComponent","summary":"Remove a component variant from a bundle","parameters":[{"name":"bundleVariantId","required":true,"in":"path","schema":{"type":"string"}},{"name":"componentVariantId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["bundles"]}},"/api/v1/bundles/{bundleVariantId}/availability":{"get":{"operationId":"BundlesController_getAvailability","summary":"Get how many of this bundle can be assembled from component stock on hand","description":"Computed live over the flattened leaf components, netting reserved quantity off available quantity — the same figure the aggregation job stores as buildableQuantity.","parameters":[{"name":"bundleVariantId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["bundles"]}},"/api/v1/brands":{"get":{"operationId":"BrandsController_findAll","summary":"List all brands with product counts","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["brands"]},"post":{"operationId":"BrandsController_create","summary":"Create a brand","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBrandDto"}}}},"responses":{"201":{"description":""}},"tags":["brands"]}},"/api/v1/brands/from-sources":{"get":{"operationId":"BrandsController_findFromSources","summary":"Distinct brands found on supplier-source SKUs, with promote status","description":"Distinct brands found on source (INBOUND) channel SKUs, with SKU/source counts\nand whether a matching catalog Brand record exists.","parameters":[],"responses":{"200":{"description":""}},"tags":["brands"]}},"/api/v1/brands/{id}":{"put":{"operationId":"BrandsController_update","summary":"Update a brand (name, slug, descriptions)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBrandDto"}}}},"responses":{"200":{"description":""}},"tags":["brands"]},"delete":{"operationId":"BrandsController_delete","summary":"Move a brand to trash (soft delete)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["brands"]},"get":{"operationId":"BrandsController_getOne","summary":"Get a single brand (name, slug, descriptions)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["brands"]}},"/api/v1/brands/trash":{"get":{"operationId":"BrandsController_listTrash","summary":"List soft-deleted brands","parameters":[],"responses":{"200":{"description":""}},"tags":["brands"]}},"/api/v1/brands/bulk-restore":{"post":{"operationId":"BrandsController_bulkRestore","summary":"Restore multiple brands from trash","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkIdsDto"}}}},"responses":{"201":{"description":""}},"tags":["brands"]}},"/api/v1/brands/{id}/restore":{"post":{"operationId":"BrandsController_restore","summary":"Restore a brand from trash","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["brands"]}},"/api/v1/brands/{id}/merge":{"post":{"operationId":"BrandsController_merge","summary":"Merge a brand into another (moves products, deletes source)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeBrandDto"}}}},"responses":{"201":{"description":""}},"tags":["brands"]}},"/api/v1/brands/export":{"get":{"operationId":"BrandsController_exportCsv","summary":"Export brands as CSV","parameters":[],"responses":{"200":{"description":""}},"tags":["brands"]}},"/api/v1/brands/import":{"post":{"operationId":"BrandsController_importCsv","summary":"Import brands from CSV (single name column)","description":"Creates a brand per row, skipping names that already exist (case-insensitive). An optional `name` header row is ignored. Returns created/skipped counts and per-row errors.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["csv"],"properties":{"csv":{"type":"string","description":"Raw CSV text: one brand name per line; optional \"name\" header row.","example":"name\nFiamma\nDometic"}}}}}},"responses":{"201":{"description":""}},"tags":["brands"]}},"/api/v1/categories":{"get":{"operationId":"CategoriesController_findAll","summary":"List all categories (org-global, hierarchical, in display order)","parameters":[],"responses":{"200":{"description":""}},"tags":["categories"]},"post":{"operationId":"CategoriesController_create","summary":"Create a category","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCategoryDto"}}}},"responses":{"201":{"description":""}},"tags":["categories"]}},"/api/v1/categories/trash":{"get":{"operationId":"CategoriesController_listTrash","summary":"List soft-deleted categories","parameters":[],"responses":{"200":{"description":""}},"tags":["categories"]}},"/api/v1/categories/bulk-restore":{"post":{"operationId":"CategoriesController_bulkRestore","summary":"Restore multiple categories from trash","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkIdsDto"}}}},"responses":{"201":{"description":""}},"tags":["categories"]}},"/api/v1/categories/{id}/restore":{"post":{"operationId":"CategoriesController_restore","summary":"Restore a category from trash","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["categories"]}},"/api/v1/categories/reorder":{"put":{"operationId":"CategoriesController_reorder","summary":"Reorder sibling categories","description":"Sets display order for the children of `parentId` (null/omitted = root level) from the `orderedIds` array. `position` is derived and read-only on item PUT.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderCategoriesDto"}}}},"responses":{"200":{"description":""}},"tags":["categories"]}},"/api/v1/categories/{id}":{"put":{"operationId":"CategoriesController_update","summary":"Update a category (name, slug, parent)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCategoryDto"}}}},"responses":{"200":{"description":""}},"tags":["categories"]},"delete":{"operationId":"CategoriesController_delete","summary":"Move a category to trash (soft delete)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["categories"]},"get":{"operationId":"CategoriesController_getOne","summary":"Get a single category (name, slug, parent, descriptions)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["categories"]}},"/api/v1/warehouses":{"get":{"operationId":"WarehousesController_findAll","summary":"List warehouses","parameters":[],"responses":{"200":{"description":""}},"tags":["warehouses"]},"post":{"operationId":"WarehousesController_create","summary":"Create a warehouse","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWarehouseDto"}}}},"responses":{"201":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/trash":{"get":{"operationId":"WarehousesController_listTrash","summary":"List trashed warehouses","parameters":[],"responses":{"200":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/{id}":{"get":{"operationId":"WarehousesController_findOne","summary":"Get a warehouse by ID","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["warehouses"]},"patch":{"operationId":"WarehousesController_update","summary":"Update a warehouse","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWarehouseDto"}}}},"responses":{"200":{"description":""}},"tags":["warehouses"]},"delete":{"operationId":"WarehousesController_delete","summary":"Soft-delete a warehouse (move to trash)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/bulk-restore":{"post":{"operationId":"WarehousesController_bulkRestore","summary":"Bulk-restore trashed warehouses","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Ids of the trashed warehouses to restore."}}}}}},"responses":{"201":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/{id}/restore":{"post":{"operationId":"WarehousesController_restore","summary":"Restore a trashed warehouse","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/{id}/bins":{"get":{"operationId":"WarehousesController_listBins","summary":"List bins at a warehouse, in effective pick order","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["warehouses"]},"post":{"operationId":"WarehousesController_createBin","summary":"Create a bin at a warehouse","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBinDto"}}}},"responses":{"201":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/{id}/bins/reorder":{"put":{"operationId":"WarehousesController_reorderBins","summary":"Set the manual bin order for a warehouse","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorderBinsDto"}}}},"responses":{"200":{"description":""}},"tags":["warehouses"]}},"/api/v1/warehouses/{id}/bins/{binId}":{"patch":{"operationId":"WarehousesController_renameBin","summary":"Rename a bin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"binId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBinDto"}}}},"responses":{"200":{"description":""}},"tags":["warehouses"]},"delete":{"operationId":"WarehousesController_deleteBin","summary":"Delete an empty bin","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"binId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["warehouses"]}},"/api/v1/option-types":{"get":{"operationId":"OptionTypesController_findAll","summary":"List the org's option types with product usage counts","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["option-types"]},"post":{"operationId":"OptionTypesController_create","summary":"Create an option type (e.g. Size, Color)","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOptionTypeDto"}}}},"responses":{"201":{"description":""}},"tags":["option-types"]}},"/api/v1/option-types/{id}":{"put":{"operationId":"OptionTypesController_update","summary":"Update an option type (name, position)","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOptionTypeDto"}}}},"responses":{"200":{"description":""}},"tags":["option-types"]},"delete":{"operationId":"OptionTypesController_delete","summary":"Delete an unused option type","description":"Fails with 409 if the option type is still used by any product option.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["option-types"]}},"/api/v1/products/{productId}/options":{"get":{"operationId":"ProductOptionsController_findAll","summary":"List a product's options in display order","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["option-types"]},"post":{"operationId":"ProductOptionsController_create","summary":"Add an option to a product","description":"Appends at the end of the display order when `position` is omitted.","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProductOptionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["option-types"]}},"/api/v1/products/{productId}/options/{optionId}":{"put":{"operationId":"ProductOptionsController_update","summary":"Update a product option (name, position)","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}},{"name":"optionId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProductOptionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["option-types"]},"delete":{"operationId":"ProductOptionsController_delete","summary":"Remove an option from a product","description":"Also deletes every variant's stored value for this option (cascade).","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}},{"name":"optionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"204":{"description":""}},"tags":["option-types"]}},"/api/v1/variants/{variantId}/options":{"get":{"operationId":"VariantOptionsController_findAll","summary":"List a variant's option values in option display order","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["option-types"]},"put":{"operationId":"VariantOptionsController_setOptions","summary":"Replace a variant's option values","description":"Full replace: existing values are deleted and the given set is written. Each `productOptionId` must be an option on the variant's product or on its parent product (hierarchical families).","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetVariantOptionsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["option-types"]}},"/api/v1/products/{id}/attachments":{"get":{"operationId":"ProductAttachmentsController_list","summary":"List a product's attachments with presigned download URLs","description":"Includes the parent product's attachments (flagged `inherited: true`) for child products. `fileUrl` is a presigned S3 download URL valid for 1 hour.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["product-attachments"]},"post":{"operationId":"ProductAttachmentsController_create","summary":"Register an uploaded file as a product attachment","description":"Step 2 of the upload flow: records the file uploaded via the presigned URL from POST /products/:id/attachments/upload-url.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label","fileName","fileUrl","contentType","sizeBytes"],"properties":{"label":{"type":"string","description":"Display label for the attachment (required, trimmed)."},"fileName":{"type":"string","description":"Original file name."},"fileUrl":{"type":"string","description":"The `fileUrl` returned by the upload-url endpoint."},"s3Key":{"type":"string","description":"The `s3Key` returned by the upload-url endpoint; derived from `fileUrl` when omitted."},"contentType":{"type":"string","description":"MIME type of the file.","example":"application/pdf"},"sizeBytes":{"type":"number","description":"File size in bytes."}}}}}},"responses":{"201":{"description":""}},"tags":["product-attachments"]}},"/api/v1/products/{id}/attachments/upload-url":{"post":{"operationId":"ProductAttachmentsController_createUploadUrl","summary":"Get a presigned S3 upload URL for a product attachment","description":"Step 1 of the upload flow: PUT the file bytes to the returned `uploadUrl` (valid 1 hour), then register it via POST /products/:id/attachments with the returned `fileUrl`/`s3Key`. Allowed types: PDF, PNG, JPEG, DOC, DOCX. Max size 10MB.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileName","contentType","sizeBytes"],"properties":{"fileName":{"type":"string","description":"Original file name; sanitized into the S3 key."},"contentType":{"type":"string","description":"MIME type of the file. Must be one of: application/pdf, image/png, image/jpeg, image/jpg, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document.","example":"application/pdf"},"sizeBytes":{"type":"number","description":"File size in bytes; max 10485760 (10MB)."}}}}}},"responses":{"201":{"description":""}},"tags":["product-attachments"]}},"/api/v1/product-attachments/{attachmentId}":{"delete":{"operationId":"ProductAttachmentsController_delete","summary":"Delete a product attachment (soft delete)","description":"The stored file is kept in S3; only the attachment record is soft-deleted.","parameters":[{"name":"attachmentId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["product-attachments"]}},"/api/v1/public/variant-images/{variantId}/{index}":{"get":{"operationId":"PublicVariantImagesController_redirect","summary":"Fetch a variant image by index (unauthenticated, 302 to a presigned URL)","description":"Short, stable URL per variant image for channel platforms to fetch from. `index` is the zero-based position in the variant's image list. S3-hosted images 302-redirect to a fresh short-lived presigned URL; external http(s) images redirect as-is; data: URIs are served inline. 404 when the variant or index does not exist.","parameters":[{"name":"variantId","required":true,"in":"path","schema":{"type":"string"}},{"name":"index","required":true,"in":"path","schema":{"type":"number"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["public-variant-images"]}},"/api/v1/channel-skus":{"post":{"operationId":"ChannelSkusController_create","summary":"Create a channel SKU (listing or supplier row) on a channel, optionally linked to a variant.","description":"Returns 409 if the channel already has a SKU with the same channelSku value.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateChannelSkuDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]},"get":{"operationId":"ChannelSkusController_findAll","summary":"List channel SKUs with filtering, sorting, and offset pagination.","description":"Filter on one custom field with `customField=<key>`: alone it returns rows where the field is set, `customFieldValue` narrows to an exact value (for a list field, rows whose list contains it), and `customFieldEmpty=true` returns rows where it is not set. `hasMap=true|false` keeps rows that do / do not show a MAP (a sales listing reads its product's MAP, a supplier row its own). Sort by a custom field with `sortBy=customField:<key>` (rows with nothing set sort last). `include=customValues` attaches each row's own custom-field values.","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"mapped","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasSource","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"brand","required":true,"in":"query","schema":{"type":"string"}},{"name":"outOfStock","required":true,"in":"query","schema":{"type":"string"}},{"name":"updatedInLastImport","required":true,"in":"query","schema":{"type":"string"}},{"name":"variantIds","required":true,"in":"query","schema":{"type":"string"}},{"name":"presence","required":true,"in":"query","schema":{"type":"string"}},{"name":"onSale","required":true,"in":"query","schema":{"type":"string"}},{"name":"saleEndsBefore","required":true,"in":"query","schema":{"type":"string"}},{"name":"customField","required":true,"in":"query","schema":{"type":"string"}},{"name":"customFieldValue","required":true,"in":"query","schema":{"type":"string"}},{"name":"customFieldEmpty","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasMap","required":true,"in":"query","schema":{"type":"string"}},{"name":"include","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortBy","required":true,"in":"query","schema":{"type":"string"}},{"name":"sortOrder","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/brands":{"get":{"operationId":"ChannelSkusController_getBrands","summary":"List the distinct brand names across channel SKUs, optionally scoped to one channel.","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["channel-skus"]}},"/api/v1/channel-skus/custom-field-values":{"get":{"operationId":"ChannelSkusController_getCustomFieldValueCounts","summary":"Count a channel's SKUs by the values of one custom field.","description":"Returns how many SKUs have the field set, how many do not, and each distinct value with the number of SKUs carrying it, most common first. A list field counts a SKU once under each of its members.","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"customField","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/bulk":{"delete":{"operationId":"ChannelSkusController_bulkDelete","summary":"Bulk-delete channel SKUs on a channel, by explicit ids, unmapped-only, or the whole channel.","description":"Hard-deletes matching SKUs. With no `ids` in the body and no `unmappedOnly=true`, this deletes EVERY SKU on the channel and requires `confirm: \"DELETE_ALL_SKUS\"` in the body. Deleting mapped supplier rows flags the affected variants for a sourcing-status recompute. With `ids`, `removeFromChannel: true` also deletes the listings on the external store (whole channel product when every present variant of it is selected, otherwise just the variants; rows whose storefront delete fails are kept for a retry), and `trashProducts: true` also moves the linked catalog items to trash (the product when all its active variants are covered, otherwise the variants; BOM-component items are reported as blocked).","parameters":[{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"unmappedOnly","required":true,"in":"query","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Specific channel SKU ids to delete. When present, other criteria are ignored."},"confirm":{"type":"string","description":"Must be \"DELETE_ALL_SKUS\" when deleting every SKU on the channel (no ids, unmappedOnly not true).","example":"DELETE_ALL_SKUS"},"removeFromChannel":{"type":"boolean","description":"Also delete the selected listings on the external store (BigCommerce/Shopify). Requires `ids`."},"trashProducts":{"type":"boolean","description":"Also move the linked Foundry products/SKUs to trash (soft delete, restorable). Requires `ids`."}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]},"put":{"operationId":"ChannelSkusController_bulkUpdate","summary":"Set one or more fields to the same value on many channel SKUs at once.","description":"Every field present in `data` is written identically to every SKU in `ids`. Requires a non-empty `ids` array and at least one field in `data`. Returns the number of SKUs updated.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids","data"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Channel SKU ids to update."},"data":{"type":"object","description":"Field values applied to every listed SKU.","properties":{"brand":{"type":"string","description":"Brand name."},"cost":{"type":"number","description":"Supplier unit cost."},"mpn":{"type":"string","description":"Manufacturer part number."},"upc":{"type":"string","description":"UPC barcode."},"gtin":{"type":"string","description":"GTIN barcode."}}}}}}}},"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/bulk/sale-window":{"put":{"operationId":"ChannelSkusController_bulkExtendSaleWindow","summary":"Move the sale end date on many channel SKUs at once","description":"Extends (or shortens) a running sale. Only rows whose sale is currently ACTIVE are changed — the rest are counted as `skipped` rather than quietly reactivated, because republishing a lapsed sale in bulk would push prices nobody reviewed. Only the end moves: `salePrice` is deliberately not bulk-editable, since one price across products of different values is how a sale lands below MAP or above the regular price. Find what is about to expire with GET /channel-skus?saleEndsBefore=...","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ids","saleEndsAt"],"properties":{"ids":{"type":"array","items":{"type":"string"},"description":"Channel SKU ids."},"saleEndsAt":{"type":"string","format":"date-time","description":"New end of the sale window, ISO 8601. Must be in the future."}}}}}},"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/export":{"get":{"operationId":"ChannelSkusController_exportCsv","summary":"Export channel SKUs to a CSV download.","description":"When `ids` is provided, all other filter params are ignored.","parameters":[{"name":"ids","required":true,"in":"query","schema":{"type":"string"}},{"name":"channelId","required":true,"in":"query","schema":{"type":"string"}},{"name":"search","required":true,"in":"query","schema":{"type":"string"}},{"name":"mapped","required":true,"in":"query","schema":{"type":"string"}},{"name":"brand","required":true,"in":"query","schema":{"type":"string"}},{"name":"outOfStock","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasSource","required":true,"in":"query","schema":{"type":"string"}},{"name":"updatedInLastImport","required":true,"in":"query","schema":{"type":"string"}},{"name":"customField","required":true,"in":"query","schema":{"type":"string"}},{"name":"customFieldValue","required":true,"in":"query","schema":{"type":"string"}},{"name":"customFieldEmpty","required":true,"in":"query","schema":{"type":"string"}},{"name":"hasMap","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/{id}":{"get":{"operationId":"ChannelSkusController_findOne","summary":"Get one channel SKU with its channel and linked variant details.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]},"put":{"operationId":"ChannelSkusController_update","summary":"Update a channel SKU's fields, including its sale price window.","description":"Field edits only. Linking to a variant goes through POST /channel-skus/{id}/link (and /unlink) so the link side effects run; a `variantId` here is rejected. `extendedFields` is the supplier-import staging blob and is not writable; per-channel values belong in PUT /channel-skus/{id}/custom-values.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateChannelSkuDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]},"delete":{"operationId":"ChannelSkusController_remove","summary":"Permanently delete a channel SKU.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/{id}/push":{"post":{"operationId":"ChannelSkusController_push","summary":"Push this one listing to its channel.","description":"Targeted alternative to POST /channels/:id/sync: pushes only this listing, not every listing on the channel. Optionally pass `fields` (sync-config field names, e.g. [\"price\",\"compareAtPrice\"]) to narrow further. A field the channel's Sync Settings do not push is skipped regardless of what is requested, and named in the response's `skipped` array — `synced: 1` with `\"mpn\"` in `skipped` means the listing was pushed without its mpn. Read a channel's Sync Settings with GET /listing/channels/{channelId}/sync-config. Values are read from the catalog variant, not from this listing's own mirror fields.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string"},"description":"Sync-config field names to push. Omit to push every field the channel is authoritative for."}}}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]}},"/api/v1/channel-skus/{id}/link":{"post":{"operationId":"ChannelSkusController_link","summary":"Link a channel SKU to a catalog variant.","description":"Side effects: gap-fills the variant's option values and custom-field values from the SKU, backfills order lines that synced before the mapping existed, and flags the variant for a sourcing-status recompute. For INBOUND (supplier) channels it also triggers inventory aggregation, and when the source has a linked warehouse it creates a stock receipt for the SKU's inventory quantity.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkChannelSkuDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]},"delete":{"operationId":"ChannelSkusController_unlink","summary":"Unlink a channel SKU from its variant.","description":"The previously linked variant is flagged for a sourcing-status recompute, since removing a supplier row can open a sourcing gap.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]}},"/api/v1/channel-skus/{id}/suggestions":{"get":{"operationId":"ChannelSkusController_getSuggestions","summary":"Suggest catalog variants that likely match this channel SKU, with a confidence level and match reason.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/{id}/change-history":{"get":{"operationId":"ChannelSkusController_getChangeHistory","summary":"List a channel SKU's field-change log entries, newest first, with offset pagination.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/bulk-import":{"post":{"operationId":"ChannelSkusController_bulkImport","summary":"Import a multi-source CSV: rows are split by their source column and one import job is queued per source.","description":"Multipart upload (`file` + optional `mapping` JSON). Each distinct source value maps to an INBOUND channel by name, auto-creating channels that do not exist yet; rows with an empty source are skipped. Returns the parent job id and the per-source breakdown.","parameters":[],"responses":{"201":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/import":{"post":{"operationId":"ChannelSkusController_import","summary":"Upload a CSV of SKUs for one channel and queue an asynchronous import job.","description":"Multipart upload (`file` + `channelId`, optional `mapping` JSON and `delimiter`). Large files are split into chunks processed in parallel under a parent job. Poll GET /channel-skus/import/{jobId}/status for progress.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportChannelSkuDto"}}}},"responses":{"201":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/import/{jobId}/status":{"get":{"operationId":"ChannelSkusController_getImportStatus","summary":"Get live status and progress for an import job, aggregating chunked or per-source child jobs.","description":"Polling this endpoint also detects chunks stalled in PROCESSING for over 3 minutes and re-queues them.","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["channel-skus"]}},"/api/v1/channel-skus/import/{jobId}/cancel":{"post":{"operationId":"ChannelSkusController_cancelImport","summary":"Cancel a pending or processing import job (including all of its child jobs) and delete its uploaded files.","description":"Cancel a running or pending import job\nFor chunked jobs, cancels parent and all child jobs","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["channel-skus"]}},"/api/v1/channel-skus/import/{jobId}/retry":{"post":{"operationId":"ChannelSkusController_retryImport","summary":"Re-queue a failed or stalled import job, resuming from the last processed row.","description":"Completed and cancelled jobs cannot be retried, and bulk-import parent jobs must be retried per source job.","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["channel-skus"]}},"/api/v1/custom-fields":{"get":{"operationId":"CustomFieldDefsController_list","summary":"List the org's custom field definitions, optionally filtered by the entity type they apply to.","description":"Archived definitions are excluded unless `includeArchived=true`. `appliesTo` accepts PRODUCT, VARIANT, CHANNEL_SKU, CATEGORY, or BRAND; unrecognized values are ignored.","parameters":[{"name":"includeArchived","required":true,"in":"query","schema":{"type":"string"}},{"name":"appliesTo","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]},"post":{"operationId":"CustomFieldDefsController_create","summary":"Create a custom field definition.","description":"The key must be unique in the org and match ^[a-z][a-z0-9_]*$. `appliesTo` defaults to [PRODUCT, VARIANT] when omitted.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomFieldDefDto"}}}},"responses":{"201":{"description":""}},"tags":["custom-fields"]}},"/api/v1/custom-fields/{id}":{"get":{"operationId":"CustomFieldDefsController_get","summary":"Get one custom field definition.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]},"patch":{"operationId":"CustomFieldDefsController_update","summary":"Update a custom field definition (its key and type are immutable).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCustomFieldDefDto"}}}},"responses":{"200":{"description":""}},"tags":["custom-fields"]},"delete":{"operationId":"CustomFieldDefsController_delete","summary":"Delete a custom field definition, or archive it instead when it still has stored values or template references.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["custom-fields"]}},"/api/v1/custom-fields/{id}/bulk-fill":{"post":{"operationId":"CustomFieldDefsController_bulkFill","summary":"Set this field's value on every product in the org that doesn't already have one.","description":"Gap-fill semantics: products that already have a value are skipped, never overwritten. The value must be sent in the column matching the field's declared type. Returns filled and skipped counts.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkFillCustomFieldDto"}}}},"responses":{"201":{"description":""}},"tags":["custom-fields"]}},"/api/v1/products/{id}/custom-values":{"get":{"operationId":"CustomFieldValuesController_listProduct","summary":"List the custom field values stored directly on a product.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]},"put":{"operationId":"CustomFieldValuesController_upsertProduct","summary":"Create or update custom field values stored directly on a product.","description":"Each entry must send its value in the one column matching the field's declared type (valueText for TEXT/LONGTEXT/RICHTEXT/URL, valueNumber for NUMBER, valueBoolean for BOOLEAN, valueDate for DATE, valueJson for SELECT/MULTISELECT/JSON/ARRAY/OBJECT); a value in any other column is rejected with 400. Omitting all five value columns clears the stored value. Returns the full stored value list.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomFieldValuesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/products/{id}/resolved-custom-values":{"get":{"operationId":"CustomFieldValuesController_resolveProduct","summary":"Resolve the effective value of every active custom field for a product.","description":"Resolution order per field: the product's own value, then the product's class-template default, then none. The `source` on each entry says which level won.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/products/{id}/custom-values/{fieldDefId}":{"delete":{"operationId":"CustomFieldValuesController_deleteProductValue","summary":"Remove one custom field's stored value from a product.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"fieldDefId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]}},"/api/v1/variants/{id}/custom-values":{"get":{"operationId":"CustomFieldValuesController_listVariant","summary":"List the custom field values stored directly on a variant.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]},"put":{"operationId":"CustomFieldValuesController_upsertVariant","summary":"Create or update custom field values stored directly on a variant.","description":"Each entry must send its value in the one column matching the field's declared type (valueText for TEXT/LONGTEXT/RICHTEXT/URL, valueNumber for NUMBER, valueBoolean for BOOLEAN, valueDate for DATE, valueJson for SELECT/MULTISELECT/JSON/ARRAY/OBJECT); a value in any other column is rejected with 400. Omitting all five value columns clears the stored value. Returns the full stored value list.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomFieldValuesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/variants/{id}/resolved-custom-values":{"get":{"operationId":"CustomFieldValuesController_resolveVariant","summary":"Resolve the effective value of every active custom field for a variant.","description":"Resolution order per field: the variant's own value, then the parent product's value, then the product's class-template default, then none. The `source` on each entry says which level won.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/variants/{id}/custom-values/{fieldDefId}":{"delete":{"operationId":"CustomFieldValuesController_deleteVariantValue","summary":"Remove one custom field's stored value from a variant.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"fieldDefId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]}},"/api/v1/channel-skus/{id}/custom-values":{"get":{"operationId":"CustomFieldValuesController_listChannelSku","summary":"List the custom field values stored directly on a channel SKU.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]},"put":{"operationId":"CustomFieldValuesController_upsertChannelSku","summary":"Create or update custom field values stored directly on a channel SKU.","description":"Each entry must send its value in the one column matching the field's declared type (valueText for TEXT/LONGTEXT/RICHTEXT/URL, valueNumber for NUMBER, valueBoolean for BOOLEAN, valueDate for DATE, valueJson for SELECT/MULTISELECT/JSON/ARRAY/OBJECT); a value in any other column is rejected with 400. Omitting all five value columns clears the stored value. Returns the full stored value list.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomFieldValuesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/channel-skus/{id}/resolved-custom-values":{"get":{"operationId":"CustomFieldValuesController_resolveChannelSku","summary":"Resolve the effective value of every active custom field for a channel SKU.","description":"Resolution order per field: the channel SKU's own value, then the linked variant's value, then that variant's product value, then none. The `source` on each entry says which level won.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/channel-skus/{id}/custom-values/{fieldDefId}":{"delete":{"operationId":"CustomFieldValuesController_deleteChannelSkuValue","summary":"Remove one custom field's stored value from a channel SKU.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"fieldDefId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]}},"/api/v1/categories/{id}/custom-values":{"get":{"operationId":"CustomFieldValuesController_listCategory","summary":"List the custom field values stored on a category.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]},"put":{"operationId":"CustomFieldValuesController_upsertCategory","summary":"Create or update custom field values stored on a category.","description":"Each entry must send its value in the one column matching the field's declared type (valueText for TEXT/LONGTEXT/RICHTEXT/URL, valueNumber for NUMBER, valueBoolean for BOOLEAN, valueDate for DATE, valueJson for SELECT/MULTISELECT/JSON/ARRAY/OBJECT); a value in any other column is rejected with 400. Omitting all five value columns clears the stored value. Returns the full stored value list.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomFieldValuesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/categories/{id}/resolved-custom-values":{"get":{"operationId":"CustomFieldValuesController_resolveCategory","summary":"Get the effective value of every active custom field for a category.","description":"Categories have no inheritance chain: each value is either stored on the category itself (source `own`) or absent (source `none`).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/categories/{id}/custom-values/{fieldDefId}":{"delete":{"operationId":"CustomFieldValuesController_deleteCategoryValue","summary":"Remove one custom field's stored value from a category.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"fieldDefId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]}},"/api/v1/brands/{id}/custom-values":{"get":{"operationId":"CustomFieldValuesController_listBrand","summary":"List the custom field values stored on a brand.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]},"put":{"operationId":"CustomFieldValuesController_upsertBrand","summary":"Create or update custom field values stored on a brand.","description":"Each entry must send its value in the one column matching the field's declared type (valueText for TEXT/LONGTEXT/RICHTEXT/URL, valueNumber for NUMBER, valueBoolean for BOOLEAN, valueDate for DATE, valueJson for SELECT/MULTISELECT/JSON/ARRAY/OBJECT); a value in any other column is rejected with 400. Omitting all five value columns clears the stored value. Returns the full stored value list.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertCustomFieldValuesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/brands/{id}/resolved-custom-values":{"get":{"operationId":"CustomFieldValuesController_resolveBrand","summary":"Get the effective value of every active custom field for a brand.","description":"Brands have no inheritance chain: each value is either stored on the brand itself (source `own`) or absent (source `none`).","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"object"}}}}}},"tags":["custom-fields"]}},"/api/v1/brands/{id}/custom-values/{fieldDefId}":{"delete":{"operationId":"CustomFieldValuesController_deleteBrandValue","summary":"Remove one custom field's stored value from a brand.","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}},{"name":"fieldDefId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["custom-fields"]}},"/api/v1/listing/{productId}/status":{"get":{"operationId":"ListingController_getListingStatus","summary":"Get listing status for a product across all channels","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Listing"],"security":[{"bearer":[]}]}},"/api/v1/listing/channels/{channelId}/categories":{"get":{"operationId":"ListingController_getChannelCategories","summary":"Get categories for a channel","parameters":[{"name":"channelId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Listing"],"security":[{"bearer":[]}]}},"/api/v1/listing/channels/{channelId}/sync-config":{"get":{"operationId":"ListingController_getChannelSyncConfig","summary":"Read a channel's Sync Settings — which fields it pulls from and pushes to the store","description":"Read-only. `push` is the ceiling for every push: POST /channel-skus/{id}/push and the automatic pushes on variant edits send only fields that are `true` here, whatever was requested. Field names match the `fields` accepted by the push endpoint.","parameters":[{"name":"channelId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Listing"],"security":[{"bearer":[]}]}},"/api/v1/listing/{productId}/list":{"post":{"operationId":"ListingController_createListing","summary":"Create or update a listing on a sales channel","description":"Safe to re-run. A SIMPLE/BUNDLE/ASSEMBLY product that is already listed is synced. A FAMILY that is already listed gets its missing children added as variants on the existing channel product (BigCommerce; existing variants are never modified) — see `variantsAdded`, `variantsAlreadyListed` and `variantsFailed` in the response. Never creates a second channel product.","parameters":[{"name":"productId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateListingDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Listing"],"security":[{"bearer":[]}]}},"/api/v1/listing/channels/{channelId}/restore-variants":{"post":{"operationId":"ListingController_restoreMissingVariants","summary":"Restore missing BigCommerce variants on one product from Foundry listing data (additive repair; BC deletes variants when product options are edited store-side)","parameters":[{"name":"channelId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RestoreVariantsDto"}}}},"responses":{"201":{"description":""}},"tags":["Listing"],"security":[{"bearer":[]}]}},"/api/v1/listing/channels/{channelId}/list-all":{"post":{"operationId":"ListingController_listAll","summary":"List all live products on a HEADLESS storefront channel (bulk)","parameters":[{"name":"channelId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"201":{"description":""}},"tags":["Listing"],"security":[{"bearer":[]}]}}},"info":{"title":"Foundry Partner API","description":"\nREST API for Foundry IMS partners and integrations — catalog, orders, purchase orders, stock, and outbound webhooks.\n\nAuthenticate every request with an admin API key: `Authorization: Bearer fims_...`. Keys are created in the admin UI (Settings → API Keys) with per-key permission scopes; a request needs the scope listed on the endpoint's permission gate. All routes live under `/api/v1`.\n\n**Custom fields** — typed custom fields (defined under `/custom-fields`) are read and written through the `/custom-values` routes, and the list endpoints attach them with `?include=customValues`. The `customFields` object on products and variants is a legacy free-form blob and is empty for catalogs that use typed fields.\n\n**Full guide** — auth and scopes, rate limits, pagination, error shapes, webhook signature verification, and the versioning policy: https://foundryims.com/docs/partner-api\n","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"}},"schemas":{"CreateEndpointDto":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"description":"Label shown in the merchant's admin — name it after your app","example":"My Shipping App"},"url":{"type":"string","description":"HTTPS endpoint deliveries are POSTed to","example":"https://example.com/foundry/webhooks"},"events":{"description":"Event names to subscribe to (see GET /webhooks/events). At least one; an unknown name is rejected with a 400 that lists every valid one.","example":["order.created","inventory.availability_changed"],"type":"array","items":{"type":"string"}}},"required":["name","url","events"]},"UpdateEndpointDto":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"description":"Label shown in the merchant's admin","example":"My Shipping App"},"url":{"type":"string","description":"HTTPS endpoint deliveries are POSTed to","example":"https://example.com/foundry/webhooks"},"events":{"description":"Replacement list of event names to subscribe to (see GET /webhooks/events)","example":["order.created","inventory.availability_changed"],"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean","description":"Set false to pause deliveries; set true to resume, which also clears the consecutive-failure counter behind the five-strike auto-disable","example":true}}},"CreatePurchaseOrderLineDto":{"type":"object","properties":{"variantId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Variant id (UUID)"},"channelSkuId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Channel SKU id (UUID)"},"isCustom":{"type":"boolean","example":false,"description":"Whether this is a custom (non-catalog) line"},"description":{"type":"string","example":"Wireless Headphones - Black","description":"Line description"},"partNumber":{"type":"string","example":"WH1000-BLK","description":"Supplier part number for the line"},"quantityOrdered":{"type":"number","minimum":1,"example":100,"description":"Quantity ordered"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost"}},"required":["quantityOrdered","unitCost"]},"CreatePurchaseOrderDto":{"type":"object","properties":{"channelId":{"type":"string","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f","description":"Supplier channel id (UUID)"},"warehouseId":{"type":"string","example":"4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90","description":"Destination warehouse id (UUID)"},"type":{"type":"string","enum":["WAREHOUSE","DROPSHIP"],"example":"WAREHOUSE","description":"Purchase order type"},"shippingAddress":{"type":"object","example":{"name":"Acme Inc","street1":"123 Main St","city":"Austin","state":"TX","postalCode":"78701","country":"US"},"description":"Ship-to address"},"expectedAt":{"type":"string","example":"2026-07-15","description":"Expected delivery date (ISO 8601)"},"notes":{"type":"string","example":"Please ship via ground freight","description":"Notes for the supplier"},"deliveryMethod":{"type":"string","enum":["EMAIL","DOWNLOAD","WEB"],"example":"EMAIL","description":"Delivery method"},"lines":{"description":"Purchase order line items","type":"array","items":{"$ref":"#/components/schemas/CreatePurchaseOrderLineDto"}}},"required":["channelId"]},"UpdatePurchaseOrderDto":{"type":"object","properties":{"warehouseId":{"type":"string","example":"4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90","description":"Destination warehouse id (UUID)"},"expectedAt":{"type":"string","example":"2026-07-15","description":"Expected delivery date (ISO 8601)"},"notes":{"type":"string","example":"Please ship via ground freight","description":"Notes for the supplier"}}},"ChangeSupplierDto":{"type":"object","properties":{"channelId":{"type":"string","example":"9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d","description":"The supplier channel id (UUID, direction INBOUND or BOTH) the purchase order should belong to"},"note":{"type":"string","maxLength":1000,"example":"Allocated to Terrawagen by mistake; GoCode supplies this part and has the order","description":"Why the supplier is being changed. Recorded on the activity log; the order notes are left alone."}},"required":["channelId"]},"AddLineDto":{"type":"object","properties":{"variantId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Variant id (UUID). Required unless isCustom is true"},"channelSkuId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Supplier channel SKU id (UUID)"},"isCustom":{"type":"boolean","example":false,"description":"Whether this is a custom (non-catalog) line; custom lines require a description and carry no variant"},"description":{"type":"string","example":"Wireless Headphones - Black","description":"Line description (required for custom lines)"},"partNumber":{"type":"string","example":"WH1000-BLK","description":"Supplier part number for the line"},"quantityOrdered":{"type":"number","minimum":1,"example":100,"description":"Quantity ordered"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost"}},"required":["quantityOrdered","unitCost"]},"UpdateLineDto":{"type":"object","properties":{"quantityOrdered":{"type":"number","minimum":1,"example":100,"description":"Quantity ordered"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost (also syncs the supplier channel SKU cost)"},"channelSkuId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Re-link the line to another of this supplier's SKUs for the same product (UUID). Allowed after the order is sent, until the line's invoice is reconciled; quantity and cost stay DRAFT-only."}}},"ReceiveLineDto":{"type":"object","properties":{"quantityReceived":{"type":"number","minimum":1,"example":25,"description":"Quantity received in this receipt. Added to the line's running total; cannot exceed the remaining unreceived quantity"}},"required":["quantityReceived"]},"LinkReservationsDto":{"type":"object","properties":{"reservationIds":{"example":["5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b"],"description":"Reservation ids (UUIDs) to link to the purchase order line for the dropship workflow","type":"array","items":{"type":"string"}},"quantities":{"type":"object","additionalProperties":{"type":"integer"},"example":{"5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b":2},"description":"Link only part of a hold: reservation id → units this PO covers. The hold is split — those units go on the PO, the rest stay on the order line as a separate hold to ship from your own stock. Omit a reservation (or give its full quantity) to link all of it. Not available for kit (bundle) component holds."}},"required":["reservationIds"]},"AddTrackingDto":{"type":"object","properties":{"carrier":{"type":"string","example":"FedEx","description":"Carrier name ('FedEx', 'UPS', 'USPS', 'DHL', etc.)"},"trackingNumber":{"type":"string","example":"123456789012","description":"Carrier tracking number"},"trackingUrl":{"type":"string","example":"https://www.fedex.com/fedextrack/?trknbr=123456789012","description":"Tracking URL; auto-generated from the carrier and tracking number if not provided"}},"required":["carrier","trackingNumber"]},"CheckDocumentLineDto":{"type":"object","properties":{"vendorSku":{"type":"string","description":"The supplier's part number, exactly as printed."},"description":{"type":"string"},"quantity":{"type":"number"},"unitCost":{"type":"number"},"lineTotal":{"type":"number"}}},"CheckDocumentDto":{"type":"object","properties":{"attachmentId":{"type":"string","description":"The attachment these figures were read from. Optional — the check works without it — but naming the document is what records that THIS document has been looked at, which is how a purchase order leaves the needs-checking queue. Omit it and the check still runs, it just clears nothing."},"documentType":{"type":"string","description":"INVOICE | ORDER_CONFIRMATION | PACKING_SLIP | RECEIPT | STATEMENT | OTHER. Use STATEMENT when the document bills more than one order — such a document cannot be filed against a single purchase order; record it as a combined bill instead, which splits it into one bill per order."},"vendorName":{"type":"string"},"vendorOrderNumber":{"type":"string","description":"The supplier's own order or reference number."},"invoiceNumber":{"type":"string"},"invoiceDate":{"type":"string","description":"ISO 8601 date, YYYY-MM-DD."},"dueDate":{"type":"string","description":"ISO 8601 date, YYYY-MM-DD."},"subtotal":{"type":"number","description":"Goods total before tax and shipping."},"taxAmount":{"type":"number"},"shippingAmount":{"type":"number"},"otherCharges":{"type":"number","description":"Handling, surcharges and fees that are neither tax nor shipping."},"discountAmount":{"type":"number","description":"Any amount taken OFF the subtotal, as a POSITIVE number: a trade discount, a promotional code, or a vendor credit applied at checkout. Without it a discounted document fails the arithmetic check, because subtotal + tax + shipping is then larger than the total and nothing explains the gap.\n\nIt says only that the document showed a deduction, not what kind. A trade discount is a genuinely lower price; a credit code is the vendor spending money it already owed us, and that one does NOT make the goods cheaper — file the invoice at full cost and settle it with POST /invoices/:id/apply-credit instead."},"totalAmount":{"type":"number","description":"The amount actually payable."},"currency":{"type":"string","description":"ISO 4217 code, e.g. USD."},"amountPaid":{"type":"number","description":"How much of this document has ALREADY been paid, as printed. Many suppliers charge the card at order time, which makes their order confirmation a receipt rather than a bill — this is the field that says so, and without it a settled document looks identical to an unpaid one."},"balanceDue":{"type":"number","description":"How much is still owed, as printed. A balance of 0 is meaningful on its own — it settles the document whether or not the total is restated beside it."},"paymentMethod":{"type":"string","description":"How it was paid, as printed — e.g. \"Visa ending 4242\", \"PayPal\", \"check 10428\". Copy the wording; it is what a person reconciling a card statement searches for."},"paymentReference":{"type":"string","description":"The payment's own reference or transaction id, as printed."},"paidDate":{"type":"string","description":"ISO 8601 date, YYYY-MM-DD — when it was paid."},"lines":{"type":"array","items":{"$ref":"#/components/schemas/CheckDocumentLineDto"}},"carrier":{"type":"string","description":"Shipping carrier, e.g. FedEx, UPS, USPS, DHL."},"shippingMethod":{"type":"string","description":"Service level as printed, e.g. Ground, 2Day, Priority."},"trackingNumber":{"type":"string"},"shipDate":{"type":"string","description":"ISO 8601 date, YYYY-MM-DD."}}},"CreateInvoiceLineDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string","example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Purchase order line id (UUID) this invoice line is matched to; must belong to the invoice's purchase order"},"vendorSku":{"type":"string","example":"WH1000-BLK","description":"The vendor's SKU as printed on the invoice"},"description":{"type":"string","example":"Wireless Headphones - Black","description":"Line description"},"quantityInvoiced":{"type":"number","minimum":1,"example":100,"description":"Quantity invoiced"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost on the invoice"}},"required":["quantityInvoiced","unitCost"]},"CreateInvoiceDto":{"type":"object","properties":{"purchaseOrderId":{"type":"string","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f","description":"Purchase order id (UUID) the invoice is for. Required when invoiceType is INVOICE"},"invoiceType":{"type":"string","enum":["INVOICE","CREDIT_MEMO","DEBIT_MEMO"],"example":"INVOICE","description":"Invoice type (defaults to INVOICE)"},"channelId":{"type":"string","example":"5f6a7b8c-9d0e-1f2a-3b4c-5d6e7f8a9b0c","description":"The supplier channel this document belongs to. Omit it when `purchaseOrderId` is given — the order already names the supplier, and a value that disagrees with it is rejected rather than silently preferred.\n\nRequired for a memo with NO purchase order, which is the supplier-level credit: a vendor who refunds a return, corrects an overcharge or issues a coupon code in lieu of cash is crediting the account, not one of our orders, and the credit gets spent on an order nobody has placed yet. Without a supplier the credit belongs to nobody — it appears on no statement and can never be applied."},"relatedInvoiceId":{"type":"string","example":"4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90","description":"Id (UUID) of the invoice a credit/debit memo relates to"},"invoiceNumber":{"type":"string","example":"INV-2026-0042","description":"The vendor's invoice number, exactly as printed. Optional, because most supplier documents do not carry one — a supplier who charges the card at order time sends a confirmation-cum-receipt with an order reference and no invoice number, and no later invoice is coming to supply one. Omit it rather than inventing one: it is unique per organization, so a made-up number collides silently with the vendor's real number if one ever does arrive. An un-numbered invoice is identified by its order and date, and the number can be filled in later if the vendor sends one."},"invoiceDate":{"type":"string","example":"2026-07-01","description":"Invoice date (ISO 8601). Optional only when sourceAttachmentId is given: a document that states no date falls back to the date that document was uploaded, and the response says so via invoiceDateSource. With no source document the date is required — there is nothing to fall back to."},"dueDate":{"type":"string","example":"2026-07-31","description":"Payment due date (ISO 8601)"},"subtotal":{"type":"number","minimum":0,"example":2250,"description":"Subtotal; computed from the lines if omitted"},"taxAmount":{"type":"number","minimum":0,"example":180,"description":"Tax amount (defaults to 0)"},"shippingAmount":{"type":"number","minimum":0,"example":45.5,"description":"Shipping amount (defaults to 0)"},"otherCharges":{"type":"number","minimum":0,"example":10,"description":"Other charges (defaults to 0)"},"totalAmount":{"type":"number","minimum":0,"example":2485.5,"description":"Total amount; computed as subtotal + tax + shipping + other charges if omitted"},"notes":{"type":"string","example":"Backordered items shipped separately","description":"Free-form notes"},"attachmentUrl":{"type":"string","example":"https://files.example.com/invoices/INV-2026-0042.pdf","description":"URL of the invoice document attachment"},"sourceAttachmentId":{"type":"string","description":"Id of the purchase-order attachment these figures were read from. Records provenance — which document produced this invoice — and must belong to this invoice's purchase order."},"markAsPaid":{"type":"boolean","example":false,"description":"Create the invoice already in PAID status. Sets paidAt to now unless paidAt is given, which is the field to prefer when filing a document that states when it was paid."},"paidAt":{"type":"string","example":"2026-09-04","description":"When it was paid (ISO 8601), as the document states. Implies PAID status on its own — a supplier who charged the card at order time has already been paid, and their receipt says when. Prefer this over markAsPaid whenever the document gives a date: 'paid at some point before now' is not the same fact and it is the one that ends up in the books."},"paymentMethod":{"type":"object","example":"Visa ending 4242","description":"How it was paid, one of CHECK | WIRE | ACH | CREDIT_CARD | OTHER. Free-form card wording from a receipt belongs in paymentReference."},"paymentReference":{"type":"string","example":"Visa ending 4242 · txn 3PX88291","description":"The payment's own reference as printed on the document — the string someone reconciling a card statement will search for."},"vendorOrderNumber":{"type":"string","example":"HQXDX595V","description":"The supplier's own order or confirmation reference, as printed — NOT their invoice number. Two receipts with different references are different bills; the reconciliation duplicate check compares it."},"lines":{"description":"Invoice line items","type":"array","items":{"$ref":"#/components/schemas/CreateInvoiceLineDto"}}}},"StatementRowDto":{"type":"object","properties":{"reference":{"type":"string","example":"INV-104233","description":"The reference printed on the line — the supplier’s invoice number, their order number, or our purchase order number. Matched exactly (trimmed, ignoring case) against all three."},"date":{"type":"string","pattern":"YMD","example":"2026-09-14","description":"The line’s date, YYYY-MM-DD."},"amount":{"type":"number","example":412.5,"description":"Positive for a charge (an invoice); negative for a payment or credit."},"description":{"type":"string","example":"Invoice — PO 20260912-003"}},"required":["amount"]},"CompareStatementDto":{"type":"object","properties":{"channelId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"The supplier (channel) the statement is from."},"from":{"type":"string","pattern":"YMD","example":"2026-09-01","description":"Start of the statement period. Defaults to the earliest row date."},"to":{"type":"string","pattern":"YMD","example":"2026-09-30","description":"End of the statement period, and the \"as of\" date for Foundry’s open balance. Defaults to the latest row date."},"closingBalance":{"type":"number","example":1832.4,"description":"The balance printed at the foot of the statement."},"rows":{"type":"array","items":{"$ref":"#/components/schemas/StatementRowDto"}}},"required":["channelId","rows"]},"ApplyCreditDto":{"type":"object","properties":{"creditMemoId":{"type":"string","example":"7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d","description":"The CREDIT_MEMO to draw down. It must belong to the same supplier as this invoice, and must not itself hang off a purchase order — a credit filed against an order has already reduced that order’s cost and cannot be spent twice."},"amount":{"type":"number","minimum":1,"example":324,"description":"How much of the credit to spend. Defaults to the smaller of what is left on the credit and what is still owed on the invoice, which is what applying a credit almost always means."},"reference":{"type":"string","example":"SPRINTERSTORE21925","description":"The vendor’s own reference for the credit as it was spent — typically the coupon code entered at checkout. This is the string someone reconciling the vendor’s statement will search for."}},"required":["creditMemoId"]},"PaymentConfirmationItemDto":{"type":"object","properties":{"invoiceId":{"type":"string","example":"7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"},"paidAt":{"type":"string","pattern":"/^\\d{4}-\\d{2}-\\d{2}$/","example":"2026-09-21","description":"The paid date to record (YYYY-MM-DD) — normally the reported paid date, or the invoice date when the receipt printed none."},"paymentReference":{"type":"string","description":"Overrides the reported payment reference. Omit to record the reference as reported."},"paymentMethod":{"type":"string","enum":["CHECK","WIRE","ACH","CREDIT_CARD","OTHER"],"description":"Overrides the reported payment method. Omit to record the method as reported."}},"required":["invoiceId","paidAt"]},"ConfirmReportedPaymentsDto":{"type":"object","properties":{"reason":{"type":"string","minLength":3,"example":"Checked against card statement","description":"Why — recorded on every invoice in the batch."},"items":{"type":"array","items":{"$ref":"#/components/schemas/PaymentConfirmationItemDto"}}},"required":["reason","items"]},"UpdateInvoiceDto":{"type":"object","properties":{"invoiceNumber":{"type":"string","example":"INV-2026-0042","description":"The vendor's invoice number"},"invoiceDate":{"type":"string","example":"2026-07-01","description":"Invoice date (ISO 8601)"},"dueDate":{"type":"string","example":"2026-07-31","description":"Payment due date (ISO 8601)"},"subtotal":{"type":"number","minimum":0,"example":2250,"description":"Subtotal"},"taxAmount":{"type":"number","minimum":0,"example":180,"description":"Tax amount"},"shippingAmount":{"type":"number","minimum":0,"example":45.5,"description":"Shipping amount"},"otherCharges":{"type":"number","minimum":0,"example":10,"description":"Other charges"},"totalAmount":{"type":"number","minimum":0,"example":2485.5,"description":"Total amount"},"notes":{"type":"string","example":"Backordered items shipped separately","description":"Free-form notes"},"attachmentUrl":{"type":"string","example":"https://files.example.com/invoices/INV-2026-0042.pdf","description":"URL of the invoice document attachment"}}},"ReviseInvoiceDto":{"type":"object","properties":{"reason":{"type":"string","minLength":3,"example":"Shipping was filed as a charge; the order carries it as a line","description":"Why the invoice is being corrected — recorded in the activity log with the before and after."},"invoiceDate":{"type":"string","example":"2026-10-02"},"dueDate":{"type":"string","example":"2026-11-01"},"taxAmount":{"type":"number","minimum":0,"example":0},"shippingAmount":{"type":"number","minimum":0,"example":0},"otherCharges":{"type":"number","minimum":0,"example":0},"notes":{"type":"string","description":"Replaces the notes."},"vendorOrderNumber":{"type":"string","description":"The supplier's own order or confirmation reference."},"sourceAttachmentId":{"type":"string","description":"The supplier document on this purchase order that the invoice was filed from. Links a bill keyed by hand to its paperwork, so it counts as documented. A document backs one invoice only."},"lines":{"description":"The complete set of lines after the correction — replaces every existing line. Omit to leave the lines alone.","type":"array","items":{"$ref":"#/components/schemas/CreateInvoiceLineDto"}}},"required":["reason"]},"AddInvoiceLineDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string","example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Purchase order line id (UUID) to match this invoice line to; must belong to the invoice's purchase order"},"vendorSku":{"type":"string","example":"WH1000-BLK","description":"The vendor's SKU as printed on the invoice"},"description":{"type":"string","example":"Wireless Headphones - Black","description":"Line description"},"quantityInvoiced":{"type":"number","minimum":1,"example":100,"description":"Quantity invoiced"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost on the invoice"}},"required":["quantityInvoiced","unitCost"]},"UpdateInvoiceLineDto":{"type":"object","properties":{"vendorSku":{"type":"string","example":"WH1000-BLK","description":"The vendor's SKU as printed on the invoice"},"description":{"type":"string","example":"Wireless Headphones - Black","description":"Line description"},"quantityInvoiced":{"type":"number","minimum":1,"example":100,"description":"Quantity invoiced"},"unitCost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost on the invoice"}}},"MatchLineDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string","example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Purchase order line id (UUID) to match the invoice line to; must belong to the invoice's purchase order"}},"required":["purchaseOrderLineId"]},"MarkPaidDto":{"type":"object","properties":{"paidAt":{"type":"string","example":"2026-07-28","description":"Payment date (ISO 8601); defaults to now"},"paymentReference":{"type":"string","example":"CHK-10422","description":"Payment reference (check number, transaction id, etc.)"},"paymentMethod":{"type":"string","enum":["CHECK","WIRE","ACH","CREDIT_CARD","OTHER"],"example":"ACH","description":"Payment method"}}},"CorrectPaymentDto":{"type":"object","properties":{"reason":{"type":"string","minLength":3,"example":"Receipt shows the card it was charged to","description":"Why — recorded in the activity log with the payment record as it was."},"paidAt":{"type":"string","example":"2026-09-21","description":"When it was paid. On an unpaid invoice this marks it paid."},"paymentReference":{"type":"string","example":"Visa •••• 0154","description":"Proof of payment as printed on the document — card last four, transaction id, check number."},"paymentMethod":{"type":"string","enum":["CHECK","WIRE","ACH","CREDIT_CARD","OTHER"]},"markUnpaid":{"type":"boolean","description":"Take a paid invoice back to unpaid, clearing its payment details — for a bill marked paid with nothing to show it was. Cannot be combined with the payment fields."}},"required":["reason"]},"SetInvoiceNumberDto":{"type":"object","properties":{"invoiceNumber":{"type":"string","example":"INVOICE 167738","description":"The vendor's invoice number, exactly as printed. Fills a number that is absent, or replaces a wrong one when `reason` is given — refused once the invoice is reconciled or posted to the accounting system. Never put an order or confirmation reference here; that is vendorOrderNumber."},"reason":{"type":"string","minLength":3,"example":"Keyed the order reference as the invoice number","description":"Required when the invoice already has a number: why it is being replaced. Recorded in the activity log with the old number."}},"required":["invoiceNumber"]},"CreateLandedCostDto":{"type":"object","properties":{"invoiceId":{"type":"string","example":"4d5e6f7a-8b9c-0d1e-2f3a-4b5c6d7e8f90","description":"Invoice id (UUID) the cost came from; must belong to the same purchase order"},"costType":{"type":"string","enum":["SHIPPING","TAX","DUTY","INSURANCE","HANDLING","BROKERAGE","OTHER"],"example":"SHIPPING","description":"Type of landed cost"},"description":{"type":"string","example":"Ocean freight, container share","description":"Description of the cost"},"amount":{"type":"number","minimum":0,"example":350,"description":"Cost amount"},"allocationMethod":{"type":"string","enum":["BY_VALUE","BY_QUANTITY","BY_WEIGHT","MANUAL"],"example":"BY_VALUE","description":"How the cost is spread across PO lines (defaults to BY_VALUE)"},"vendorName":{"type":"string","example":"Global Freight Co","description":"Vendor the cost was paid to"},"referenceNumber":{"type":"string","example":"BOL-88213","description":"Reference number (bill of lading, invoice number, etc.)"}},"required":["costType","amount"]},"UpdateLandedCostDto":{"type":"object","properties":{"costType":{"type":"string","enum":["SHIPPING","TAX","DUTY","INSURANCE","HANDLING","BROKERAGE","OTHER"],"example":"SHIPPING","description":"Type of landed cost"},"description":{"type":"string","example":"Ocean freight, container share","description":"Description of the cost"},"amount":{"type":"number","minimum":0,"example":350,"description":"Cost amount; changing it re-runs allocation unless the method is MANUAL"},"allocationMethod":{"type":"string","enum":["BY_VALUE","BY_QUANTITY","BY_WEIGHT","MANUAL"],"example":"BY_VALUE","description":"How the cost is spread across PO lines; changing it re-runs allocation unless set to MANUAL"},"vendorName":{"type":"string","example":"Global Freight Co","description":"Vendor the cost was paid to"},"referenceNumber":{"type":"string","example":"BOL-88213","description":"Reference number (bill of lading, invoice number, etc.)"}}},"ManualAllocationDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string","example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Purchase order line id (UUID)"},"allocatedAmount":{"type":"number","minimum":0,"example":125.5,"description":"Amount of the landed cost allocated to this line"}},"required":["purchaseOrderLineId","allocatedAmount"]},"SetManualAllocationsDto":{"type":"object","properties":{"allocations":{"description":"Per-line allocations replacing any existing ones; amounts must sum to the landed cost amount (within $0.01)","type":"array","items":{"$ref":"#/components/schemas/ManualAllocationDto"}}},"required":["allocations"]},"CombinedBillLineDto":{"type":"object","properties":{"kind":{"type":"string","enum":["ITEM","CHARGE"],"description":"ITEM for goods, CHARGE for shipping, a return label or a fee"},"vendorSku":{"type":"string","example":"FNV-FTA","description":"The supplier's SKU exactly as printed (ITEM)"},"description":{"type":"string","example":"Fridge vent - Black"},"quantity":{"type":"number","minimum":1,"example":7,"description":"ITEM: total quantity billed on this line"},"unitCost":{"type":"number","minimum":0,"example":48,"description":"ITEM: unit cost as printed"},"lineTotal":{"type":"number","minimum":0,"example":336,"description":"The line amount as printed. ITEM: defaults to quantity × unit cost. CHARGE: required."},"chargeType":{"type":"string","enum":["SHIPPING","RETURN_FEE","OTHER"],"description":"CHARGE: SHIPPING, RETURN_FEE or OTHER"},"allocationMethod":{"type":"string","enum":["BY_VALUE","BY_QUANTITY","EQUAL","MANUAL"],"description":"CHARGE: how it is split across the orders on the bill. Defaults to BY_VALUE."}},"required":["kind"]},"CreateCombinedBillDto":{"type":"object","properties":{"channelId":{"type":"string","description":"The supplier (channel id)"},"invoiceNumber":{"type":"string","example":"6DC1CDA3-0023","description":"The supplier's invoice number as printed. Never invent one."},"invoiceDate":{"type":"string","example":"2026-05-01"},"dueDate":{"type":"string","example":"2026-05-31"},"periodStart":{"type":"string","example":"2026-04-01","description":"First day of the orders the invoice covers, if it says"},"periodEnd":{"type":"string","example":"2026-04-30","description":"Last day of the orders the invoice covers, if it says"},"totalAmount":{"type":"number","example":2192,"description":"The invoice total as printed"},"notes":{"type":"string"},"attachmentUrl":{"type":"string"},"paidAt":{"type":"string","example":"2026-05-03","description":"When it was paid, if the document or the payer says so"},"paymentMethod":{"type":"string","enum":["CHECK","WIRE","ACH","CREDIT_CARD","OTHER"]},"paymentReference":{"type":"string","example":"Visa ending 0154"},"lines":{"type":"array","items":{"$ref":"#/components/schemas/CombinedBillLineDto"}}},"required":["channelId","invoiceDate","totalAmount","lines"]},"ReplaceCombinedBillLinesDto":{"type":"object","properties":{"lines":{"type":"array","items":{"$ref":"#/components/schemas/CombinedBillLineDto"}}},"required":["lines"]},"ItemAllocationDto":{"type":"object","properties":{"lineId":{"type":"string"},"purchaseOrderLineId":{"type":"string"},"quantity":{"type":"number","minimum":1}},"required":["lineId","purchaseOrderLineId","quantity"]},"ChargeMethodDto":{"type":"object","properties":{"lineId":{"type":"string"},"method":{"type":"string","enum":["BY_VALUE","BY_QUANTITY","EQUAL","MANUAL"]}},"required":["lineId","method"]},"ChargeAllocationDto":{"type":"object","properties":{"lineId":{"type":"string"},"purchaseOrderId":{"type":"string"},"amount":{"type":"number","minimum":0}},"required":["lineId","purchaseOrderId","amount"]},"SetAllocationsDto":{"type":"object","properties":{"items":{"description":"Replaces every goods allocation. Omit to keep the current ones.","type":"array","items":{"$ref":"#/components/schemas/ItemAllocationDto"}},"chargeMethods":{"description":"Change how a charge is split. BY_VALUE, BY_QUANTITY and EQUAL are recomputed from the goods allocations.","type":"array","items":{"$ref":"#/components/schemas/ChargeMethodDto"}},"manualCharges":{"description":"Amounts for MANUAL charges, replacing that charge’s split. Ignored for other methods.","type":"array","items":{"$ref":"#/components/schemas/ChargeAllocationDto"}}}},"UnpostCombinedBillDto":{"type":"object","properties":{"reason":{"type":"string","example":"Split the shipping wrong"}}},"MarkCombinedBillPaidDto":{"type":"object","properties":{"paidAt":{"type":"string","example":"2026-05-03"},"paymentMethod":{"type":"string","enum":["CHECK","WIRE","ACH","CREDIT_CARD","OTHER"]},"paymentReference":{"type":"string"}}},"RefreshLineHoldsDto":{"type":"object","properties":{"relink":{"type":"boolean","description":"Re-resolve the catalog item from the line's channel SKU / variant id first; the stored link is replaced only when the identifiers match a different item","default":false}}},"ReresolveLinesDto":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"Report only; write nothing","default":true},"channelId":{"type":"string","description":"Limit to one channel"},"orderId":{"type":"string","description":"Limit to one order"},"limit":{"type":"number","minimum":1,"maximum":5000,"default":500,"description":"Maximum lines to examine"}}},"ReresolvedLineDto":{"type":"object","properties":{"orderNumber":{"type":"string","nullable":true,"description":"Channel order number of the order the line belongs to"},"sku":{"type":"string","nullable":true,"description":"The SKU as the order line carries it"},"via":{"type":"string","description":"How the line matched","enum":["channelVariantId","sku","foundrySku","skuBase","unmatched","ambiguous"]},"matchedSku":{"type":"string","description":"For a skuBase match: the catalogue SKU actually paired to after stripping the listing suffix — the one inferred step in the chain, surfaced so it can be spot-checked"},"variantId":{"type":"string","nullable":true,"description":"Catalog variant the line resolved to; null when unmatched"},"linked":{"type":"boolean","description":"False on a dry run"},"reserved":{"type":"boolean","description":"Whether the line holds at least one reservation after this run (observed, not predicted). Always false on a dry run."},"note":{"type":"string","description":"Why the line could not be linked or reserved, when it could not"}},"required":["orderNumber","sku","via","variantId","linked","reserved"]},"ReresolveLinesResultDto":{"type":"object","properties":{"dryRun":{"type":"boolean","description":"True when nothing was written — the run only reported what it would do"},"examined":{"type":"number","description":"Unlinked lines examined this run"},"matched":{"type":"number","description":"Lines that resolved to a catalog variant"},"unmatched":{"type":"number","description":"Lines no catalog variant could be found for"},"reservationsCreated":{"type":"number","description":"Reservations actually created by this run"},"truncated":{"type":"boolean","description":"True when the run hit its line limit — run again for the rest"},"lines":{"description":"Per-line outcome for every examined line","type":"array","items":{"$ref":"#/components/schemas/ReresolvedLineDto"}}},"required":["dryRun","examined","matched","unmatched","reservationsCreated","truncated","lines"]},"BulkUpdateOrderStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"Status to move the order to","enum":["PENDING","PROCESSING","AWAITING_FULFILLMENT","AWAITING_SHIPMENT","PARTIALLY_SHIPPED","SHIPPED","COMPLETED","CANCELLED","REFUNDED","PARTIALLY_REFUNDED","ON_HOLD"],"example":"SHIPPED"},"note":{"type":"string","maxLength":1000,"description":"Note recorded in the order's status history","example":"Customer picked up in person"},"orderIds":{"description":"IDs of the orders to update — every order gets the same status and note","example":["0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"],"type":"array","items":{"type":"string"}}},"required":["status","orderIds"]},"UpdateOrderStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"Status to move the order to","enum":["PENDING","PROCESSING","AWAITING_FULFILLMENT","AWAITING_SHIPMENT","PARTIALLY_SHIPPED","SHIPPED","COMPLETED","CANCELLED","REFUNDED","PARTIALLY_REFUNDED","ON_HOLD"],"example":"SHIPPED"},"note":{"type":"string","maxLength":1000,"description":"Note recorded in the order's status history","example":"Customer picked up in person"}},"required":["status"]},"FulfillOrderDto":{"type":"object","properties":{"warehouseId":{"type":"string","description":"Warehouse the goods shipped from"},"markShipped":{"type":"boolean","description":"Also set the order status to SHIPPED","default":true}}},"AdjustStockDto":{"type":"object","properties":{"variantId":{"type":"string","description":"Variant whose stock is being adjusted","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"warehouseId":{"type":"string","description":"Warehouse the adjustment applies to","example":"7b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"},"quantity":{"type":"number","description":"Signed change to on-hand quantity: positive adds stock, negative removes it","example":-3},"reason":{"type":"string","enum":["CYCLE_COUNT","DAMAGE","THEFT","FOUND","CORRECTION","OTHER"],"description":"Why the adjustment was made — recorded on the resulting stock movement","example":"CYCLE_COUNT"},"notes":{"type":"string","description":"Free-text note recorded on the resulting stock movement","example":"Two units water-damaged in receiving"},"binId":{"type":"string","nullable":true,"description":"Bin location to adjust. Omitted: resolves automatically (primary bin, else the single bin, else the fullest). Explicit null: the Unassigned location. A bin id: that exact bin.","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"},"allowNegative":{"type":"boolean","description":"Confirm an adjustment that would take the target location below zero on-hand; refused without it","default":false,"example":false}},"required":["variantId","warehouseId","quantity","reason"]},"SetPrimaryBinDto":{"type":"object","properties":{"variantId":{"type":"string","description":"Variant (SKU) whose primary bin is being set","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"warehouseId":{"type":"string","description":"Warehouse the primary-bin designation applies to (each warehouse has its own)","example":"7b1a2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d"},"binId":{"type":"string","description":"Bin to designate as primary — the SKU must already have a stock location in this bin","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"}},"required":["variantId","warehouseId","binId"]},"CountLineDto":{"type":"object","properties":{"sku":{"type":"string","description":"The SKU counted — matched by foundrySku first, falling back to the org's own sku","example":"CPL0122"},"bin":{"type":"string","description":"Bin NAME the count was taken in; blank or omitted means the Unassigned location","example":"A-01-03"},"counted":{"type":"number","description":"Quantity physically counted at that location","example":12},"primary":{"type":"boolean","description":"True makes this bin the SKU's primary bin; absent leaves the primary as-is","example":true}},"required":["sku","counted"]},"BulkCountPreviewDto":{"type":"object","properties":{"warehouseId":{"type":"string","description":"Populated from the :warehouseId path parameter — do not send it in the body"},"lines":{"description":"The count lines to diff against current on-hand quantities","type":"array","items":{"$ref":"#/components/schemas/CountLineDto"}}},"required":["lines"]},"CountApplyLineDto":{"type":"object","properties":{"variantId":{"type":"string","description":"Variant the line applies to; either variantId or sku must identify the line","example":"0d9b1d5e-7c2a-4f6e-9b1a-2c3d4e5f6a7b"},"sku":{"type":"string","description":"Alternative to variantId: the SKU, matched by foundrySku first, falling back to the org's own sku — resolved the same way preview does","example":"CPL0122"},"binId":{"type":"string","nullable":true,"description":"Bin location the count applies to; null or omitted means the Unassigned location","example":"3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"},"counted":{"type":"number","description":"Quantity physically counted at that location","example":12},"primary":{"type":"boolean","description":"True makes this bin the SKU's primary bin; absent leaves the primary as-is","example":true}},"required":["counted"]},"BulkCountApplyDto":{"type":"object","properties":{"warehouseId":{"type":"string","description":"Populated from the :warehouseId path parameter — do not send it in the body"},"lines":{"description":"The count lines to apply as stock adjustments","type":"array","items":{"$ref":"#/components/schemas/CountApplyLineDto"}},"notes":{"type":"string","description":"Free-text note recorded on the resulting stock movements","example":"Q3 cycle count, aisle A"},"scope":{"type":"string","enum":["FULL","PARTIAL"],"description":"PARTIAL (the default) touches only the lines listed; FULL additionally zeros every stocked location in the warehouse that the sheet does not mention","default":"PARTIAL","example":"PARTIAL"}},"required":["lines"]},"CreateProductDto":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"example":"Wireless Headphones","description":"Product display name"},"sku":{"type":"string","description":"Team-facing SKU for the default variant","maxLength":255,"example":"WH-1000"},"slug":{"type":"string","maxLength":255,"example":"wireless-headphones","description":"URL handle for storefronts"},"description":{"type":"string","example":"Over-ear Bluetooth headphones with active noise cancellation","description":"Public product description"},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"example":"Wireless Headphones | Acme","description":"SEO meta title; blank falls back to the product name"},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"example":"Over-ear Bluetooth headphones with ANC.","description":"SEO meta description; blank falls back to a description excerpt"},"internalDescription":{"type":"string","example":"Top seller; reorder when stock < 50","description":"Internal-only notes"},"featured":{"type":"boolean","example":false,"description":"Whether the product is featured"},"displayOrder":{"type":"number","example":0,"description":"Sort order for display"},"type":{"type":"string","enum":["SIMPLE","FAMILY","BUNDLE","ASSEMBLY"],"example":"SIMPLE","description":"Product type"},"brandId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Brand id (UUID)"},"categoryId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Category id (UUID)"},"productClassId":{"type":"string","nullable":true,"example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Product class id (UUID), or null"},"customFields":{"type":"object","example":{"color":"black","warrantyMonths":24},"description":"LEGACY free-form JSON blob kept for older integrations. Not the typed custom fields: those are defined under /custom-fields and read/written via /products/{id}/custom-values (or GET /products?include=customValues)."}},"required":["name"]},"DuplicateProductDto":{"type":"object","properties":{"name":{"type":"string","description":"Name for the copy; defaults to \"<original name> (copy)\"","maxLength":500,"example":"Wireless Headphones (copy)"},"sku":{"type":"string","description":"Team-facing SKU for the copy's variant; left blank when omitted (a fresh Foundry SKU is always generated)","maxLength":200,"example":"WH-1000-V2"}}},"UpdateProductDto":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"example":"Wireless Headphones","description":"Product display name"},"slug":{"type":"string","maxLength":255,"example":"wireless-headphones","description":"URL handle for storefronts"},"description":{"type":"string","example":"Over-ear Bluetooth headphones with active noise cancellation","description":"Public product description"},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"example":"Wireless Headphones | Acme","description":"SEO meta title; blank falls back to the product name"},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"example":"Over-ear Bluetooth headphones with ANC.","description":"SEO meta description; blank falls back to a description excerpt"},"internalDescription":{"type":"string","example":"Top seller; reorder when stock < 50","description":"Internal-only notes"},"featured":{"type":"boolean","example":false,"description":"Whether the product is featured"},"displayOrder":{"type":"number","example":0,"description":"Sort order for display"},"type":{"type":"string","enum":["SIMPLE","FAMILY","BUNDLE","ASSEMBLY"],"example":"SIMPLE","description":"Product type"},"brandId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Brand id (UUID)"},"categoryId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Category id (UUID)"},"productClassId":{"type":"string","nullable":true,"example":"2b3c4d5e-6f7a-8b9c-0d1e-2f3a4b5c6d7e","description":"Product class id (UUID), or null"},"customFields":{"type":"object","example":{"color":"black","warrantyMonths":24},"description":"LEGACY free-form JSON blob kept for older integrations. Not the typed custom fields: those are defined under /custom-fields and read/written via /products/{id}/custom-values (or GET /products?include=customValues)."}}},"CreateVariantParentDto":{"type":"object","properties":{"name":{"type":"string","maxLength":255,"example":"Wireless Headphones","description":"Display name for the new FAMILY parent product"},"description":{"type":"string","example":"Over-ear Bluetooth headphones, all colors","description":"Public description for the parent product"},"brandId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Brand id (UUID) for the parent product"},"categoryId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Category id (UUID) for the parent product"},"childProductIds":{"example":["3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"],"description":"Ids (UUID) of existing products to link as children; each must not already belong to another family","type":"array","items":{"type":"string"}}},"required":["name","childProductIds"]},"LinkChildrenDto":{"type":"object","properties":{"productIds":{"example":["3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"],"description":"Ids (UUID) of products to link as children of the parent","type":"array","items":{"type":"string"}}},"required":["productIds"]},"ReorderChildrenDto":{"type":"object","properties":{"orderedChildIds":{"example":["3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f"],"description":"Child product ids (UUID) in the desired display order; each child's position becomes its index in this array, and children not listed keep their existing position","type":"array","items":{"type":"string"}}},"required":["orderedChildIds"]},"ConvertProductDto":{"type":"object","properties":{"targetType":{"type":"string","enum":["SIMPLE","FAMILY","BUNDLE","ASSEMBLY"],"example":"BUNDLE","description":"Product type to convert to"},"acknowledgeWarnings":{"type":"boolean","description":"Set true to proceed past warn-only preflight findings (links attached, stock to zero, …)"}},"required":["targetType"]},"BulkProductDefinition":{"type":"object","properties":{"name":{"type":"string","example":"Wireless Headphones","description":"Product display name"},"description":{"type":"string","example":"Over-ear Bluetooth headphones","description":"Public product description"},"brandId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Brand id (UUID); derived from the first source SKU that carries a brand when omitted"},"categoryId":{"type":"string","example":"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d","description":"Category id (UUID)"},"type":{"type":"string","enum":["SIMPLE","FAMILY","BUNDLE","ASSEMBLY"],"example":"SIMPLE","description":"SIMPLE (one product with one variant) or FAMILY (a parent with one child variant per source SKU)"},"channelSkuIds":{"example":["5e6f7a8b-9c0d-1e2f-3a4b-5c6d7e8f9a0b"],"description":"Source channel SKU ids (UUID) to link; each must exist and not already be linked to a variant","type":"array","items":{"type":"string"}}},"required":["name","type","channelSkuIds"]},"BulkCreateFromSourceDto":{"type":"object","properties":{"products":{"description":"Product definitions to create; each is processed independently and per-product failures are reported in `errors`","type":"array","items":{"$ref":"#/components/schemas/BulkProductDefinition"}}},"required":["products"]},"CreateVariantDto":{"type":"object","properties":{"productId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Parent product id (UUID)"},"sku":{"type":"string","nullable":true,"maxLength":100,"example":"WH-1000-BLK","description":"Team-facing SKU; blank/null falls back to the Foundry SKU"},"name":{"type":"string","maxLength":255,"example":"Wireless Headphones - Black","description":"Variant display name"},"upc":{"type":"string","example":"012345678905","description":"UPC barcode"},"gtin":{"type":"string","example":"00012345678905","description":"GTIN barcode"},"mpn":{"type":"string","example":"WH1000-BLK","description":"Manufacturer part number"},"cost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost"},"price":{"type":"number","minimum":0,"example":49.99,"description":"Selling price"},"compareAtPrice":{"type":"number","nullable":true,"minimum":0,"example":59.99,"description":"Compare-at / MSRP price for sale display"},"weight":{"type":"number","minimum":0,"example":0.35,"description":"Weight"},"length":{"type":"number","minimum":0,"example":20,"description":"Length"},"width":{"type":"number","minimum":0,"example":18,"description":"Width"},"height":{"type":"number","minimum":0,"example":8,"description":"Height"},"condition":{"type":"string","enum":["NEW","REFURBISHED","USED","OPEN_BOX"],"example":"NEW","description":"Product condition"},"images":{"example":["https://cdn.example.com/wh-1000-black.jpg"],"description":"Image URLs","type":"array","items":{"type":"string"}},"customFields":{"type":"object","example":{"color":"black"},"description":"LEGACY free-form JSON blob kept for older integrations. Not the typed custom fields: those are defined under /custom-fields and read/written via /variants/{id}/custom-values (or GET /variants?include=customValues)."},"trackInventory":{"type":"boolean","example":true,"description":"Whether inventory is tracked for this variant"},"isRawMaterial":{"type":"boolean","example":false,"description":"Whether this variant is a raw material"},"stockUom":{"type":"string","enum":["ea","dz","pair","mg","g","kg","oz","lb","ml","cl","L","fl_oz","cup","pt","qt","gal","mm","cm","m","in","ft","yd","bag","box","pack","roll","set","sheet","label"],"example":"EA","description":"Stock unit of measure"},"safetyStockQty":{"type":"number","minimum":0,"example":10,"description":"Safety stock quantity"},"targetStockQty":{"type":"number","minimum":0,"example":10,"description":"Build-to / target stock level"},"reorderPoint":{"type":"number","nullable":true,"minimum":0,"example":5,"description":"Build-queue trigger: only queue a below-target build once supply drops under this. Null = trigger at targetStockQty."}},"required":["productId","name"]},"UpdateVariantDto":{"type":"object","properties":{"sku":{"type":"string","nullable":true,"maxLength":100,"example":"WH-1000-BLK","description":"Team-facing SKU; blank/null falls back to the Foundry SKU"},"name":{"type":"string","maxLength":255,"example":"Wireless Headphones - Black","description":"Variant display name"},"upc":{"type":"string","example":"012345678905","description":"UPC barcode"},"gtin":{"type":"string","example":"00012345678905","description":"GTIN barcode"},"mpn":{"type":"string","example":"WH1000-BLK","description":"Manufacturer part number"},"cost":{"type":"number","minimum":0,"example":22.5,"description":"Unit cost"},"price":{"type":"number","minimum":0,"example":49.99,"description":"Selling price"},"compareAtPrice":{"type":"number","nullable":true,"minimum":0,"example":59.99,"description":"Compare-at / MSRP price for sale display"},"map":{"type":"number","nullable":true,"minimum":0,"example":44.99,"description":"Minimum advertised price. Setting it here locks the variant's MAP against source (supplier) propagation; POST /variants/:id/use-source-map releases the lock."},"weight":{"type":"number","minimum":0,"example":0.35,"description":"Weight"},"length":{"type":"number","minimum":0,"example":20,"description":"Length"},"width":{"type":"number","minimum":0,"example":18,"description":"Width"},"height":{"type":"number","minimum":0,"example":8,"description":"Height"},"condition":{"type":"string","enum":["NEW","REFURBISHED","USED","OPEN_BOX"],"example":"NEW","description":"Product condition"},"images":{"example":["https://cdn.example.com/wh-1000-black.jpg"],"description":"Image URLs","type":"array","items":{"type":"string"}},"customFields":{"type":"object","example":{"color":"black"},"description":"LEGACY free-form JSON blob kept for older integrations. Not the typed custom fields: those are defined under /custom-fields and read/written via /variants/{id}/custom-values (or GET /variants?include=customValues)."},"trackInventory":{"type":"boolean","example":true,"description":"Whether inventory is tracked for this variant"},"isRawMaterial":{"type":"boolean","example":false,"description":"Whether this variant is a raw material"},"stockUom":{"type":"string","enum":["ea","dz","pair","mg","g","kg","oz","lb","ml","cl","L","fl_oz","cup","pt","qt","gal","mm","cm","m","in","ft","yd","bag","box","pack","roll","set","sheet","label"],"example":"EA","description":"Stock unit of measure"},"safetyStockQty":{"type":"number","minimum":0,"example":10,"description":"Safety stock quantity"},"targetStockQty":{"type":"number","minimum":0,"example":10,"description":"Build-to / target stock level"},"reorderPoint":{"type":"number","nullable":true,"minimum":0,"example":5,"description":"Build-queue trigger: only queue a below-target build once supply drops under this. Null = trigger at targetStockQty."},"costSourceChannelSkuId":{"type":"string","nullable":true,"example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Pin a supplier (inbound) ChannelSku in this org as the cost and MAP source; null clears the pin. The row does NOT have to be linked to this variant, so one supplier row can drive many variants (a feed keyed on a bare part number that sells in several colours or lines). Cost and MAP flow from the pinned row unless costIsManual / mapIsManual lock them; stock still flows only through a link. 400 if the row is not an inbound row in this org."},"primarySourceChannelSkuId":{"type":"string","nullable":true,"example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Mark one of this variant's linked supplier (inbound) ChannelSkus as its primary vendor; null clears it. The fulfill screen suggests the primary vendor when warehouse stock can't cover a line — a suggestion only, never an allocation. Does not affect cost (see costSourceChannelSkuId). 400 if the row is not an inbound row in this org linked to this variant."}}},"AddComponentDto":{"type":"object","properties":{"componentVariantId":{"type":"string","example":"8f1c2e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b","description":"Id (UUID) of the variant to include in the bundle"},"quantity":{"type":"number","default":1,"minimum":1,"example":2,"description":"How many of that variant one bundle contains (defaults to 1)"}},"required":["componentVariantId"]},"UpdateQuantityDto":{"type":"object","properties":{"quantity":{"type":"number","minimum":1,"example":2,"description":"New per-bundle quantity for the component"}},"required":["quantity"]},"CreateBrandDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"Brand name.","example":"Fiamma"},"slug":{"type":"string","maxLength":200,"description":"Storefront slug; derived from name (unique per org) when omitted.","example":"fiamma"},"description1":{"type":"string","nullable":true,"maxLength":100000,"description":"Primary rich-text (HTML) description; sanitized on write."},"description2":{"type":"string","nullable":true,"maxLength":100000,"description":"Secondary rich-text (HTML) description; sanitized on write."},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"description":"SEO meta title (plain text); blank falls back to the brand name."},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"description":"SEO meta description (plain text); blank falls back to a description excerpt."},"aliases":{"description":"Other names suppliers use for this brand (abbreviations, legal names). Mapping rules match a supplier brand equal to the name or any alias; case, spaces and punctuation are ignored. Replaces the whole list.","maxItems":50,"example":["ATM","Auto Meter Products Inc"],"type":"array","items":{"type":"string","maxLength":200}}},"required":["name"]},"UpdateBrandDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"New brand name."},"slug":{"type":"string","maxLength":200,"description":"New storefront slug (unique per org)."},"description1":{"type":"string","nullable":true,"maxLength":100000,"description":"Primary rich-text (HTML) description; sanitized on write."},"description2":{"type":"string","nullable":true,"maxLength":100000,"description":"Secondary rich-text (HTML) description; sanitized on write."},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"description":"SEO meta title (plain text); blank falls back to the brand name."},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"description":"SEO meta description (plain text); blank falls back to a description excerpt."},"aliases":{"description":"Other names suppliers use for this brand (abbreviations, legal names). Mapping rules match a supplier brand equal to the name or any alias; case, spaces and punctuation are ignored. Replaces the whole list.","maxItems":50,"example":["ATM","Auto Meter Products Inc"],"type":"array","items":{"type":"string","maxLength":200}}}},"BulkIdsDto":{"type":"object","properties":{"ids":{"description":"Ids of the records to act on.","type":"array","items":{"type":"string"}}},"required":["ids"]},"MergeBrandDto":{"type":"object","properties":{"targetBrandId":{"type":"string","description":"Brand that absorbs the source brand's products; the source brand is deleted."}},"required":["targetBrandId"]},"CreateCategoryDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"Category name.","example":"Awnings"},"slug":{"type":"string","maxLength":200,"description":"Storefront slug; derived from name (unique per org) when omitted.","example":"awnings"},"parentId":{"type":"string","description":"Parent category id for hierarchical taxonomies; omit for a root category."},"description1":{"type":"string","nullable":true,"maxLength":100000,"description":"Primary rich-text (HTML) description; sanitized on write."},"description2":{"type":"string","nullable":true,"maxLength":100000,"description":"Secondary rich-text (HTML) description; sanitized on write."},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"description":"SEO meta title (plain text); blank falls back to the category name."},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"description":"SEO meta description (plain text); blank falls back to a description excerpt."}},"required":["name"]},"ReorderCategoriesDto":{"type":"object","properties":{"parentId":{"type":"string","nullable":true,"description":"The parent whose children are being reordered; null/omitted = root level."},"orderedIds":{"description":"Category ids in the desired display order. Position is derived from this array — Category.position is read-only on item PUT.","type":"array","items":{"type":"string"}}},"required":["orderedIds"]},"UpdateCategoryDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200,"description":"Category name.","example":"Awnings"},"slug":{"type":"string","maxLength":200,"description":"Storefront slug; derived from name (unique per org) when omitted.","example":"awnings"},"parentId":{"type":"string","description":"Parent category id for hierarchical taxonomies; omit for a root category."},"description1":{"type":"string","nullable":true,"maxLength":100000,"description":"Primary rich-text (HTML) description; sanitized on write."},"description2":{"type":"string","nullable":true,"maxLength":100000,"description":"Secondary rich-text (HTML) description; sanitized on write."},"metaTitle":{"type":"string","nullable":true,"maxLength":255,"description":"SEO meta title (plain text); blank falls back to the category name."},"metaDescription":{"type":"string","nullable":true,"maxLength":500,"description":"SEO meta description (plain text); blank falls back to a description excerpt."}},"required":["name"]},"CreateWarehouseDto":{"type":"object","properties":{"name":{"type":"string","description":"Warehouse name.","example":"Main Warehouse"},"address":{"type":"object","description":"Free-form address map; no shape is enforced.","additionalProperties":{"type":"string"},"example":{"line1":"123 Main St","city":"Portland","state":"OR","zip":"97201"}}},"required":["name"]},"UpdateWarehouseDto":{"type":"object","properties":{"name":{"type":"string","description":"New warehouse name."},"address":{"type":"object","nullable":true,"description":"Free-form address map; no shape is enforced. Null clears the stored address.","additionalProperties":{"type":"string"}},"isActive":{"type":"boolean","description":"Whether the warehouse is active."},"manualBinOrder":{"type":"boolean","description":"Take manual control of bin order. Enabling seeds each bin position from its current natural rank, but only when no bin has been hand-ordered yet, so re-enabling restores the last manual order rather than starting over."}}},"ReorderBinsDto":{"type":"object","properties":{"orderedIds":{"description":"Bin ids in the desired order. `position` is derived from this array. Must name every bin in the warehouse exactly once — a partial list is rejected rather than reordering a subset.","type":"array","items":{"type":"string"}}},"required":["orderedIds"]},"CreateBinDto":{"type":"object","properties":{"name":{"type":"string","description":"Bin label, unique (case-insensitive) within its warehouse.","example":"A-1"}},"required":["name"]},"UpdateBinDto":{"type":"object","properties":{"name":{"type":"string","description":"New bin label, unique (case-insensitive) within its warehouse.","example":"A-2"}},"required":["name"]},"CreateOptionTypeDto":{"type":"object","properties":{"name":{"type":"string","description":"Option type name, unique per org (e.g. \"Size\", \"Color\").","example":"Color"},"position":{"type":"number","description":"Sort position among the org's option types; appended at the end when omitted."}},"required":["name"]},"UpdateOptionTypeDto":{"type":"object","properties":{"name":{"type":"string","description":"New option type name."},"position":{"type":"number","description":"New sort position among the org's option types."}}},"CreateProductOptionDto":{"type":"object","properties":{"optionTypeId":{"type":"string","description":"Global option type to link this product option to; must exist in the org.","format":"uuid"},"name":{"type":"string","description":"Option name as shown on this product (e.g. \"Size\").","example":"Size"},"position":{"type":"number","description":"Sort position among the product's options; appended at the end when omitted."}},"required":["name"]},"UpdateProductOptionDto":{"type":"object","properties":{"name":{"type":"string","description":"New option name."},"position":{"type":"number","description":"New sort position among the product's options."}}},"SetVariantOptionValueDto":{"type":"object","properties":{"productOptionId":{"type":"string","description":"Product option this value is for; must be an option on the variant's product or its parent product.","format":"uuid"},"value":{"type":"string","description":"Value of the option for this variant (e.g. \"Large\").","example":"Large"}},"required":["productOptionId","value"]},"SetVariantOptionsDto":{"type":"object","properties":{"options":{"description":"The variant's complete set of option values; replaces all existing values.","type":"array","items":{"$ref":"#/components/schemas/SetVariantOptionValueDto"}}},"required":["options"]},"CreateChannelSkuDto":{"type":"object","properties":{"channelId":{"type":"string","description":"Id of the channel (source or storefront) this SKU belongs to."},"variantId":{"type":"string","description":"Id of the catalog variant to link this SKU to. Omit to create it unmapped and link later."},"channelSku":{"type":"string","description":"The SKU identifier as the channel knows it. Unique per channel (duplicates return 409).","example":"ABC-123"},"channelProductId":{"type":"string","description":"The channel's own product id for this listing."},"channelVariantId":{"type":"string","description":"The channel's own variant id for this listing."},"cost":{"type":"number","minimum":0,"description":"Supplier unit cost.","example":12.5},"moq":{"type":"number","minimum":1,"description":"Minimum order quantity from this supplier. Defaults to 1.","example":1},"packSize":{"type":"number","minimum":1,"description":"Units per pack when ordering from this supplier. Defaults to 1.","example":1},"leadTimeDays":{"type":"number","minimum":0,"description":"Supplier lead time in days."},"inventoryQuantity":{"type":"number","minimum":0,"description":"Quantity available at the channel (e.g. the supplier's stock). Defaults to 0."},"price":{"type":"number","minimum":0,"description":"Regular selling price on this channel.","example":99.99},"salePrice":{"type":"number","nullable":true,"description":"Temporary sale price for this listing. Must be below the regular price (and at or above MAP when the org enforces MAP). Null clears the sale and restores the regular price.","minimum":0.01,"example":79.99},"saleStartsAt":{"type":"string","nullable":true,"description":"When the sale starts. Null = immediately.","example":"2026-09-01T00:00:00Z"},"saleEndsAt":{"type":"string","nullable":true,"description":"When the sale ends and the regular price returns. Null = until cleared.","example":"2026-09-15T23:59:00Z"},"isListed":{"type":"boolean","description":"Whether this SKU is actively listed on the channel. Defaults to false."},"title":{"type":"string","description":"Listing title as it appears on the channel."},"description":{"type":"string","description":"Listing description as it appears on the channel."},"brand":{"type":"string","description":"Brand name."},"gtin":{"type":"string","description":"GTIN barcode."},"mpn":{"type":"string","description":"Manufacturer part number."},"imageUrl":{"type":"string","pattern":"/^(https?:\\/\\/|data:image\\/)/","description":"Primary image: an http(s) URL or an inline data:image/* URI.","example":"https://example.com/images/abc-123.jpg"}},"required":["channelId","channelSku"]},"UpdateChannelSkuDto":{"type":"object","properties":{"channelSku":{"type":"string","description":"The SKU identifier as the channel knows it. Unique per channel."},"channelProductId":{"type":"string","description":"The channel's own product id for this listing."},"channelVariantId":{"type":"string","description":"The channel's own variant id for this listing."},"cost":{"type":"number","minimum":0,"description":"Supplier unit cost.","example":12.5},"map":{"type":"number","minimum":0,"description":"Minimum advertised price (MAP) for this SKU."},"compareAtPrice":{"type":"number","nullable":true,"minimum":0,"description":"MSRP shown as the channel's strikethrough price. Null clears it on the channel.","example":99.95},"moq":{"type":"number","minimum":1,"description":"Minimum order quantity from this supplier.","example":1},"packSize":{"type":"number","minimum":1,"description":"Units per pack when ordering from this supplier.","example":1},"leadTimeDays":{"type":"number","minimum":0,"description":"Supplier lead time in days."},"inventoryQuantity":{"type":"number","minimum":0,"description":"Quantity available at the channel (e.g. the supplier's stock)."},"price":{"type":"number","minimum":0,"description":"Regular selling price on this channel.","example":99.99},"salePrice":{"type":"number","nullable":true,"description":"Temporary sale price for this listing. Must be below the regular price (and at or above MAP when the org enforces MAP). Null clears the sale and restores the regular price.","minimum":0.01,"example":79.99},"saleStartsAt":{"type":"string","nullable":true,"description":"When the sale starts. Null = immediately.","example":"2026-09-01T00:00:00Z"},"saleEndsAt":{"type":"string","nullable":true,"description":"When the sale ends and the regular price returns. Null = until cleared.","example":"2026-09-15T23:59:00Z"},"isListed":{"type":"boolean","description":"Whether this SKU is actively listed on the channel."},"title":{"type":"string","description":"Listing title as it appears on the channel."},"description":{"type":"string","description":"Listing description as it appears on the channel."},"brand":{"type":"string","description":"Brand name."},"upc":{"type":"string","description":"UPC barcode."},"gtin":{"type":"string","description":"GTIN barcode."},"mpn":{"type":"string","description":"Manufacturer part number."},"imageUrl":{"type":"string","pattern":"/^(https?:\\/\\/|data:image\\/)/","description":"Primary image: an http(s) URL or an inline data:image/* URI.","example":"https://example.com/images/abc-123.jpg"}}},"LinkChannelSkuDto":{"type":"object","properties":{"variantId":{"type":"string","description":"Id of the catalog variant to link the channel SKU to."}},"required":["variantId"]},"ImportChannelSkuDto":{"type":"object","properties":{"channelId":{"type":"string","description":"Id of the channel to import the CSV rows into."},"delimiter":{"type":"string","description":"CSV delimiter character when it is not a comma (e.g. \";\" or a tab).","example":";"}},"required":["channelId"]},"CreateCustomFieldDefDto":{"type":"object","properties":{"key":{"type":"string","description":"Machine key, unique per org. Must start with a lowercase letter and contain only lowercase letters, digits, or underscores. Immutable after creation.","example":"country_of_origin"},"label":{"type":"string","description":"Human-readable label shown in the UI.","example":"Country of Origin"},"description":{"type":"string","description":"Help text describing what the field is for."},"type":{"type":"string","enum":["TEXT","LONGTEXT","RICHTEXT","NUMBER","BOOLEAN","SELECT","MULTISELECT","DATE","URL","JSON","ARRAY","OBJECT"],"description":"Value type of the field. Determines which value column stores its values. Immutable after creation.","example":"TEXT"},"appliesTo":{"type":"array","enum":["PRODUCT","VARIANT","CHANNEL_SKU","CATEGORY","BRAND"],"description":"Which catalog entities this field may be attached to.","example":["PRODUCT","VARIANT"],"items":{"type":"string","enum":["PRODUCT","VARIANT","CHANNEL_SKU","CATEGORY","BRAND"]}},"config":{"type":"object","additionalProperties":true,"description":"Type-specific configuration, e.g. the option list for SELECT/MULTISELECT fields."},"validation":{"type":"object","additionalProperties":true,"description":"Validation rules applied to values of this field."},"isGlobal":{"type":"boolean","description":"Whether the field is part of the global field library. Defaults to false."},"visibleOnStorefront":{"type":"boolean","description":"Expose this field's value on the public Storefront (headless) API."},"storefrontGroup":{"type":"string","nullable":true,"description":"Storefront spec-table section this field is grouped under.","example":"Performance"},"storefrontOrder":{"type":"number","description":"Sort order within the storefront spec table (ascending)."}},"required":["key","label","type"]},"UpdateCustomFieldDefDto":{"type":"object","properties":{"label":{"type":"string","description":"Human-readable label shown in the UI."},"description":{"type":"string","description":"Help text describing what the field is for."},"config":{"type":"object","additionalProperties":true,"description":"Type-specific configuration, e.g. the option list for SELECT/MULTISELECT fields."},"validation":{"type":"object","additionalProperties":true,"description":"Validation rules applied to values of this field."},"archived":{"type":"boolean","description":"Archive (true) or unarchive (false) the field. Archived fields are hidden from listings and resolution."},"appliesTo":{"type":"array","enum":["PRODUCT","VARIANT","CHANNEL_SKU","CATEGORY","BRAND"],"description":"Which catalog entities this field may be attached to.","example":["PRODUCT","VARIANT"],"items":{"type":"string","enum":["PRODUCT","VARIANT","CHANNEL_SKU","CATEGORY","BRAND"]}},"isGlobal":{"type":"boolean","description":"Whether the field is part of the global field library."},"visibleOnStorefront":{"type":"boolean","description":"Expose this field's value on the public Storefront (headless) API."},"storefrontGroup":{"type":"string","nullable":true,"description":"Storefront spec-table section this field is grouped under. Pass null to clear.","example":"Performance"},"storefrontOrder":{"type":"number","description":"Sort order within the storefront spec table (ascending)."}}},"BulkFillCustomFieldDto":{"type":"object","properties":{"valueText":{"type":"string","nullable":true,"description":"Value for TEXT, LONGTEXT, RICHTEXT, and URL fields. Send the value in the one column matching the field's declared type."},"valueNumber":{"type":"number","nullable":true,"description":"Value for NUMBER fields."},"valueBoolean":{"type":"boolean","nullable":true,"description":"Value for BOOLEAN fields."},"valueDate":{"type":"string","nullable":true,"description":"Value for DATE fields (ISO 8601).","example":"2026-08-30"},"valueJson":{"type":"object","description":"Value for SELECT, MULTISELECT, JSON, ARRAY, and OBJECT fields."}}},"CustomFieldValueEntryDto":{"type":"object","properties":{"fieldDefId":{"type":"string","description":"Id of the custom field definition this value belongs to."},"valueText":{"type":"string","nullable":true,"description":"Value for TEXT, LONGTEXT, RICHTEXT, and URL fields. Send the value in the ONE column matching the field's declared type — any other column is rejected with 400. Omit all five value columns to clear the value."},"valueNumber":{"type":"number","nullable":true,"description":"Value for NUMBER fields."},"valueBoolean":{"type":"boolean","nullable":true,"description":"Value for BOOLEAN fields."},"valueDate":{"type":"string","nullable":true,"description":"Value for DATE fields (ISO 8601).","example":"2026-08-30"},"valueJson":{"type":"object","description":"Value for SELECT, MULTISELECT, JSON, ARRAY, and OBJECT fields."}},"required":["fieldDefId"]},"UpsertCustomFieldValuesDto":{"type":"object","properties":{"values":{"description":"Values to create or update, one entry per custom field.","type":"array","items":{"$ref":"#/components/schemas/CustomFieldValueEntryDto"}}},"required":["values"]},"CreateListingDto":{"type":"object","properties":{"channelId":{"type":"string","example":"2bda5fc9-ddcb-4fd9-bd49-d7d2c4e64baa","description":"Id (UUID) of the outbound sales channel to list on"},"categoryId":{"type":"string","example":"42","description":"Channel-side category id to file the listing under (see GET /listing/channels/{channelId}/categories). Omit to use the channel's default."},"applyEnrichment":{"type":"boolean","default":true,"description":"Apply the channel's enrichment rules (title/description templates) to the pushed listing. Defaults to true."}},"required":["channelId"]},"RestoreVariantsDto":{"type":"object","properties":{"channelProductId":{"type":"string","example":"2682","description":"BigCommerce product id whose missing variants should be restored from Foundry listing data"},"pruneUnusedOptions":{"type":"boolean","default":false,"description":"Also delete BigCommerce variant options no current variant references (dead required pickers that render only unavailable combos). Off by default."}},"required":["channelProductId"]}}}}