# ShelfCycle Public API v1.47.0 ShelfCycle's public API keeps the route major at /api/v1 and the contract version at v1.47.0. Call GET /api/v1/me first. It returns apiVersion "v1", apiContractVersion "v1.47.0", minimumCompatibleContractVersion "v1.21", organization base currency, org, key preview, scopes, actor, documentation links, and effective capabilities after current ShelfCycle access, including available tag actions. Capabilities do not prove a specific record can be written; use dryRun=true for target visibility, eligibility, exact affected-row planning, and domain readiness. Authentication: send Authorization: Bearer sk_... . Never paste or log the raw key. Prefer X-ShelfCycle-Client and X-ShelfCycle-Request-Source headers so ShelfCycle can identify the workflow. Source of truth: /api/v1/openapi.json is the machine-readable contract. This file is guidance for coding agents; the OpenAPI schemas define the exact request and response shapes. Version tuple example: apiContractVersion v1.47.0 with minimumCompatibleContractVersion v1.21. Sent email in v1.46.0: GET /api/v1/email-activity counts emails ShelfCycle sent and email notes you can open, lists them, or lists customers with no email in the window. GET /api/v1/sent-emails/{id} reads one sent email. Both require sent-emails:read, and each kind also requires that record's read scope (invoices, orders, purchase orders, supplier payments, or customers). Notes require notes:read. Sent means handed to the mail provider. File rows on the API are 900-second signed links and contain no extracted text. Created record context in v1.42.1: supported finders for orders, purchase orders, opportunities, customers, suppliers, Product Directory, lots, Cost Book, Price Book, and certifications can filter by who entered a record and when. Results include Created By and Created At, and supported commercial reports can group by Created By. Sales reports can use Entered date, while supplier discovery can continue through matching pages. See OpenAPI for each route's fields and filters, and GET /me for current availability. Contract compatibility: never compare version strings lexically or require exact equality. Strip one leading v; require exactly two or three all-numeric segments; pad a missing patch segment with zero; then compare [major, minor, patch] segment by segment. A client-required tuple is supported only when it is greater than or equal to minimumCompatibleContractVersion and less than or equal to apiContractVersion. Malformed versions fail closed to contract inspection. Thus v1.32 becomes [1,32,0], a v1.31 client accepts server v1.32, and v1.9 is below minimum v1.21. After the tuple check, confirm the required /me capabilities/scopes and OpenAPI operationIds. Core workflow: - Start every workflow by calling GET /api/v1/me, then choose the narrowest matching primitive. If a request returns repair metadata, fix the request once from error.repairCategory, error.suggestedAction, error.acceptedFilters, error.validValues, error.requiredCombination, and error.example. Do not replay an identical request when error.doNotRetrySameRequest is true. - Finder pagination and retry: - Cursor-only family: findPublicAPIPurchaseExecution, findPublicAPIOrders, findPublicAPIPurchaseOrders, findPublicAPISuppliers, and listPublicAPICertificationTypes. Send filters and the route's documented limit on the first request. When meta.nextCursor is non-null, send only cursor=; do not repeat filters or limit. - Repeat-filter family: listPublicAPITags, findPublicAPITaggedRecords, and findPublicAPIProductDirectory. Keep the original filters and route-specific limit unchanged, and echo the exact meta.nextCursor value in cursor on each continuation request. - In both families, honor each route's documented limit schema, stop when meta.nextCursor is null, and do not retry an unchanged request when error.doNotRetrySameRequest is true. Fix the request from its error envelope or restart from the original first-page query. - GET /api/v1/search is deprecated. Existing integrations may keep using it, but new binding work should use Entity Resolution and typed directories. - Use GET /api/v1/entity-resolution?q=&class= when an agent needs an authorized stable ID before a detail, report, or write-readiness call. Classes are customer, supplier, product, product_family, sales_order, purchase_order, salesperson, and warehouse. The response distinguishes auto_selected, needs_clarification, and no_match and includes match evidence for each candidate. A class=product request can select a product family when the wording identifies the family more clearly than one product; check selectedCandidate.matchedClass before using the returned ID. Matching only reads the first 12 whitespace/punctuation-separated tokens of q. relatedTags appears only when the key also has tags:read and the acting person has current Tags access. - Tag entityType values are supplier, customer, product, product_family, contact, opportunity, sales_order, purchase_order, ship_to_address, and ship_from_address. Use GET /api/v1/tags to page stable tag IDs, labels, descriptions, and applicableEntityTypes. Unrestricted tags list every class; POST/PATCH use that same field, and sending the full roster (or omitting it on create) keeps a tag unrestricted. Labels are trimmed and lowercased consistently on writes and exact-label reads. POST /api/v1/tags creates a tag; PATCH /api/v1/tags/{id} changes label, description, or applicableEntityTypes. GET /api/v1/tagged-records pages identities carrying one exact tag. GET /api/v1/tag-assignments reads one record's tags. POST and DELETE /api/v1/tag-assignments assign or remove one tag for up to 50 record IDs as one complete group. For contacts and addresses, use the record ID returned by the corresponding public detail route: customer or supplier contact, customer ship-to address, or supplier ship-from address. Other record types use the ID returned by their public detail route. - Use GET /api/v1/product-directory to page the certified product and family population by UN/NA number, CAS, hazard class, packing group, or all name tokens. Entity Resolution remains the operation for choosing one ID. - Use root finder endpoints for typed narrowed lookup: GET /customers, /suppliers, /products, /orders, /purchase-orders, and /opportunities. These are finders, not sync lists. The customer finder accepts ownership, commercial-setup, and qualifying shipped-sales filters and returns account owners, primary-location commercial terms, location coverage, shipped-sales details, and access coverage. Continue customer pages with the returned cursor by itself. Each page reflects current data at the time of that request. Opportunity list cursors bind the API key and normalized filters; order finders accept exact identifiers, status, counterparty, product, linked-order, business-date, or changed-record filters and reject unfiltered requests. Existing orderNo, customerOrderNo, and externalId exact finders accept up to 25 comma-separated tokens where documented; for example GET /api/v1/orders?customerOrderNo=A,B,C&limit=5 collapses three exact lookups into one request. A 26-token value is rejected. - Use GET /api/v1/reference/customers and GET /api/v1/reference/suppliers only to page active customer/supplier matching hints for local caches. Use GET /api/v1/representatives, GET /api/v1/payment-terms, and GET /api/v1/reference/ledger-accounts for company setup references; representative identity is always qualified by kind. Directory routes require company-reference:read and the acting person's current access, with documented pagination. An authorized Company Reference write connection can also read one selected account after a create or update. These routes are not full CRM or accounting exports. - Use GET /api/v1/product-families, GET /api/v1/product-families/{id}, and GET /api/v1/reference/product-packaging for product setup references. Product-family lookup requires q or externalId. Packaging lookup is capped and active-only. - Use GET /api/v1/reports to discover governed report keys, dimensions, measures, params, and allowedForThisKey. Use GET /api/v1/reports/{reportKey} only for the listed report keys and params. Reports paginate by offset; finders and reference directories use their own documented pagination. - Customer AR Intelligence reports are customer-invoice-settlement-behavior, customer-credit-exposure, and customer-invoice-charges. Payment behavior uses native paid invoices and final posted cash dates; credit-only and written-off settlements stay separate. Omitted dates use go-live through today, capped at 1,825 days; includeOpeningDocuments opts into opening history. Exposure keeps posted, committed, total, and credit-limit states distinct and never supplies a creditworthiness decision. Charge history subtracts only independently matching credits and labels every transaction-currency evidence row. - Use GET /api/v1/inventory-balances for certified physical-balance rows and the pre-availability-filter stock-out census; it has no lot receipt-age. Use GET /api/v1/lots for exact stored lot headers and remaining-physical inventory-addition age evidence, then GET /api/v1/lots/{id} with an exact UUID for bounded remaining-layer detail. Lots are named and quantities stay per row and basis; never sum unlike lots. Use GET /api/v1/reorder-status for configured thresholds, allocated and committed demand, projected availability, projected deficit, status counts, and configured-product coverage; it also has no lot receipt-age. projectedDeficit is max(reorderPoint - projectedAvailable, 0) in that row's package-count or base-UOM basis; it is a threshold gap, never a recommended order quantity. Never total projectedDeficit across rows or quantity bases. Continue signed cursors alone. - Hydrate exact resources only after selecting a specific id. Use GET /customers/{id}, /suppliers/{id}, /products/{id}, /orders/{id}, /purchase-orders/{id}, /opportunities/{id}, /contacts/{id}, /notes/{id}, /customers/{id}/locations/{locationId}, /customers/{id}/locations/{locationId}/addresses/{addressId}, and /suppliers/{id}/ship-from-locations/{locationId}. Sales- and purchase-order detail lineLimit defaults to 100 and has a maximum of 500; continue with lineCursor alone. - Use parent-scoped child lists only after choosing the parent: GET /customers/{id}/locations, GET /customers/{id}/locations/{locationId}/addresses, GET /suppliers/{id}/ship-from-locations, and GET /{products,customers,suppliers}/{id}/documents. Location/address/ship-from lists return active child records only; document lists return document rows that omit dismissed files and are not broad sync endpoints. Customer location list rows include an address preview; if addressesTruncated is true, use the location's /addresses list for complete active address enumeration. - Use GET /contacts only with required parentType and parentId for a visible customer or supplier. - Use GET /notes only with a required visible customer, supplier, or product subject. Optional happenedAtFrom/happenedAtTo organization-calendar days and noteType narrow the metadata-only list. Fetch a note body only after choosing an exact visible note. - Use GET /note-activity for exact organization-wide note counts grouped by day, note type, author, or subject, or for a newest-first excerpt list. It requires notes:read as an additional credential gate plus live Notes read authority (broad note.view or the complete scoped Notes bundle), accepts an organization-local window of at most 366 days, and uses an opaque cursor-only continuation for list view. - For supported POST/PATCH/PUT/DELETE writes, optionally call the same endpoint with dryRun=true before execution. dryRun checks target visibility, idempotency, duplicate candidates, optimistic concurrency, eligible child-record shape, commercial references, product reference eligibility, uploaded-object integrity, and verification guidance without writing. The customer agent's own policy decides whether to submit; ShelfCycle does not require a human approval step. - Customer, supplier, contact, customer-address, combined customer-location-with-address, and supplier ship-from CREATE requests accept optional top-level duplicateResolution=create_distinct. Omit it to keep the standard similarity review. Use it only when a separate legitimate source record is deliberate and every matching candidate is available for review. Exact source-ID, location-name, archived-record, reference, address-role, hidden-candidate, and result-limit conflicts still block the request. A dry run is optional, and execution checks the latest records again. - Execute supported POST/PATCH/PUT/DELETE writes without dryRun after your policy allows it. Use Idempotency-Key and If-Match where each operation documents them. Tag vocabulary and assignment changes are safe to repeat with the same intended state and require neither header. - Read invoices, bills, customer payments, and supplier payments through their list and detail endpoints. Find exact amounts or ranges in integer cents, select documents or credits with kind, and use matchedOn to understand payment, deposit, or batch amount matches. Continue with nextCursor by itself. Connected documents and payments appear when the key and acting person can read both families. Receivables follow current customer and salesperson access. - Reports add ap-aging, payments-received, and payments-made. Check each report's requiredScopes before calling it. Aging includes open credits, net totals, counterparty status, opening amounts, drafts, due-through amounts, and amounts over 365 days. Payment summaries report cash by counterparty or month, with invoiced or billed amounts when available. Cash, credits, write-offs, and discounts remain separate. Supplier cash follows the posted payment-batch date. Scopes: - invoices:read, bills:read, customer-payments:read, supplier-payments:read: the corresponding read routes and family-gated reports. They grant no writes or ledger balances. Credit facts require the corresponding live credit-note permission. public-api:* inherits all four. - search:read: GET /search, GET /entity-resolution, detail readback for customer/supplier/contact/product detail, and exact parent-scoped child/document reads after selecting a concrete customer, supplier, or product parent. - tags:read: GET /tags, GET /tagged-records, GET /tag-assignments, and relatedTags on Entity Resolution when the acting person has Tags access and can view the selected record type. - tags:write: POST /tags, PATCH /tags/{id}, and POST or DELETE /tag-assignments when the acting person has Tags access and can update the selected record type. Each assignment request can contain 1 to 50 record IDs, applies the full set together, and can include archived records with includeArchived=true. - contacts:read: GET /contacts and GET /contacts/{id}. - contacts:create: POST /contacts. - contacts:update: PATCH /contacts/{id} plus POST /contacts/{id}/{archive|unarchive}. Archive and restore require the acting person's current permission for the contact's customer or supplier. - notes:read: subject-scoped GET /notes, human/API-created GET /notes/{id}, and organization-wide GET /note-activity counts or excerpt lists under live Notes read authority (broad note.view or the complete scoped Notes bundle). notes:read is an additional credential gate; incomplete custom roles are denied. - notes:write: POST /notes, PATCH API-created notes, and API-created GET /notes/{id}; note externalId is a source identity with organization-scoped uniqueness. Note linkedRecords also accept {type:"user", id} for an organization user (id from your own org); each user becomes an @mention and is notified in the app, and PATCH linkedRecords replaces the list. Notes return user linked records the same way. An id never linked in the organization returns 404; a user since removed from the organization stays readable but blocks a PATCH that keeps or adds them with 422 archived_record. - opportunities:read: deny-by-default GET /opportunities and GET /opportunities/{id} for visible customer- or supplier-side opportunities. Product-linked rows also require Product view permission. Invisible and absent ids both return 404. - opportunities:write: POST/PATCH visible opportunities, POST /opportunities/{id}/close, PUT /opportunities/{id}/meta, and write-compatible GET detail. Create requires exactly one existing visible customerId or supplierId and a stable Idempotency-Key. PATCH requires If-Match, cannot close, reopen, unassign, or change estimate currency. Close accepts only IN_PROGRESS and returns 409 for a competing or invalid transition. - customers:read, suppliers:read, products:read: root finders, detail reads, and exact parent-scoped document reads for those resources. products:read also permits GET /product-directory under live Product view permission. customers:read and suppliers:read also allow parent-scoped active child reads. - documents:upload: POST /api/v1/document-uploads to mint a short-lived private PUT grant. It authorizes storage bytes only and is checked only when minting a grant. A valid descriptor may be passed to another key in the same organization; association or SDS assignment requires that route's target write scope and current ShelfCycle access, not documents:upload. Descriptor possession grants no read access. It is included in full access; restricted keys may receive it explicitly. - customers:write: customer profile create and update, customer and customer-address archive and restore, plus selected-customer location, address, and document association. Customer and location writes support declared commercial fields, including stored credit in organization base currency, payment terms, account owner, and customer-service representative; nested addresses support source identity and billing/shipping roles. It does not allow customer-location lifecycle, primary switching, account-manager write, tax, workflow defaults, upload-grant minting, document deletion, or broad document sync. - suppliers:write: supplier profile create and update, supplier and ship-from archive and restore, plus selected-supplier ship-from location and document association. Supplier writes support tax ID, payment terms, credit in organization base currency, default UOM, eligible cost ledger account, and supplier representative. It does not allow supplier root address mutation, ACH/bank credentials, accounting configuration, upload-grant minting, document deletion, or broad document sync. - products:write: product and product-family create or update, typed Product attribute assignments, packaging, aliases, supplier relationships, customer part numbers, Product archive and restore, attached documents, and current Product SDS assignment when the acting person has matching Product access. Product code attribute definitions and values are available for product setup. It does not mint upload grants and does not allow supplier create, taxonomy mutation, document deletion, inventory, prices, costs, orders, lots, receiving, accounting, account duplicate resolution, or bulk import. - orders:read: governed sales-order finder and read-only detail with stable line traversal. - purchase-orders:read: governed purchase-order finder and read-only detail with stable line traversal. It does not grant purchase execution or purchase-order document access. - purchase-order-execution:read: GET /purchase-order-execution, GET /purchase-order-lines/{id}/receipt-events, and GET /purchase-orders/{id}/documents under live Purchase Order view permission. Execution rows include nullable warehouse identity and accept an optional warehouseId filter; drafts remain opt-in through includeDraft. A warehouse-keyed comparison with another endpoint is a comparison of separately observed reads, not a certified composition. Execution traversal is current-state, not snapshot-isolated across requests; continue with nextCursor alone and use line id + executionChangedAt as a conservative refetch hint while periodically reconciling full current state. - inventory:read: GET /inventory-balances under live Product view-or-viewAll AND Lot view permissions. Returns current physical product-by-warehouse on-hand, allocated, available, quarantined-on-hand, quantity basis, and a certified census before availability filtering; no lots, value, history, incoming, demand, projection, planning, or write. Use reorder:read for configured reorder status. - lots:read: GET /lots and GET /lots/{id} for named lot quantities, remaining-stock age, and lot-level COA readiness. coaAttached is true when the selected lot has its own usable COA attachment. Supplier details require matching supplier access. - reorder:read: GET /reorder-status under live Product view-or-viewAll AND Lot view permissions. Returns configured thresholds, allocated and committed demand, projected availability, projected deficit, status counts, and configured-product coverage. Projected deficit is a same-row threshold gap, not a recommended order quantity, and must not be summed across rows or quantity bases. It excludes incoming/on-order quantities, order/customer identities, valuation, forecasting, and mutation. - purchase-order-documents:write: POST /purchase-orders/{id}/documents under live Purchase Order update permission. It associates evidence only and never receives inventory, mutates the purchase order, or posts AP/GL. - company-reference:read: customer/supplier matching hints plus representative, payment-term, and eligible ledger-account metadata directories. It excludes contacts, notes, order history, prices, costs, margin, payments, tax, ledger balances, and raw address details. - company-reference:write: create payment terms and update their descriptive fields; create, update, archive, and restore salesperson, customer-service, and supplier representatives; and create Chart of Accounts records or update their name, source ID, category, report inclusion, and eligible account classification. The acting person's current access applies to every action. - shipping-options:read: GET /shipping-options and GET /shipping-options/{id} for the six Shipping Options vocabularies. Payment terms remain available through Company Reference. - shipping-options:write: POST /shipping-options, PATCH /shipping-options/{id}, and Shipping Options archive or restore actions. Full-access keys include these actions; restricted keys need the matching Shipping Options scopes. - reports:read: governed report rollups through GET /api/v1/reports and GET /api/v1/reports/{reportKey}, including transaction-currency AR aging, Customer AR Intelligence, and deny-by-default opportunity pipeline summary. It requires current ShelfCycle access per report and excludes broad row-level invoice or credit export, raw SQL, scheduling, files, AP/GL, inventory valuation, cost recalculation, and mutation. - cost-book-entries:read: GET /api/v1/cost-book-entries and GET /api/v1/products/{productId}/cost-context with live pricingEngine.view. Cost Book entries expose ship-from location and Incoterms; no ship-from location recorded means the supplier's general fallback cost, except a Built from entry, which has no place of its own and names its bulk price's Arrangement. It is included in full access and never consults the Pricing Engine feature flag. - cost-book-entries:write: POST /api/v1/cost-book-entries, PATCH /api/v1/cost-book-entries/{id}, DELETE /api/v1/cost-book-entries/{id}, and POST /api/v1/cost-book-entries/batch with current Cost Book management access. It is included in full access; restricted keys may receive it explicitly. A successful update creates or supersedes verified Cost Book truth; edit creates an immutable correction while preserving the selected event; delete removes a mistaken Cost Entry of any shape and its corrections from the live book and History, restores the newest preceding version, and preserves saved Price Book figures; batch executes up to 50 independent commands with stable item identities. These writes are not proposals or pending approval. - current-cost-sources:read: GET /api/v1/products/{productId}/current-cost-source with live pricingEngine.view. It returns exact supplier-and-ship-from-location source authority, candidate lanes, unavailable state, and persisted selection provenance; it never silently substitutes another location or supplier. - current-cost-sources:write: PUT /api/v1/products/{productId}/current-cost-source with current Cost Book management access. It sets, replaces, or clears variable-product Current Source authority and supports dryRun=true. It does not change Cost Book entries, product supplier type, purchase evidence, or accounting data. - price-book-entries:read: GET /api/v1/price-book-entries with live priceBook.manage and customer row visibility. It is included in full access, never consults the Pricing Engine feature flag, and returns stored claims, never a selected winner. - price-book-entries:write: POST /api/v1/price-book-entries with live priceBook.manage and customer row visibility. It is included in full access, never consults the Pricing Engine feature flag, and restricted keys may receive it explicitly. Public writes accept the organization base currency only and never perform FX conversion. Commercial-books decision table: - Use Price Book for customer-facing sale-price claims by product, optional customer/ship-to/warehouse, terms, quantity floor, and transaction currency. FIXED values are stored cents per package; Public API FIXED writes may submit exact cents per package or per product base UOM. VARIABLE values are submitted and stored cents per product base UOM. - Use Cost Book for supplier-facing product cost truth and explicit direct or constructed cost evidence. Do not use Price Book as cost truth or Cost Book as sale-price truth. - Use product cost context only to compare one Cost Book lane with completed or pending purchase evidence. Purchase evidence is context, never either commercial book. - If the requested basis, currency, UOM, package lineage, lane, effective window, or visible row cannot be proved exactly, return the documented unresolved/warning state or stop for repair. Never choose a winner, convert currency, infer a zero, or silently switch books. Price Book entries: - GET defaults to view=active. scheduled, expired, all, and lane-anchored history are explicit views. Active returns every active claim, including overlaps; it never invents an authoritative/current price. Exact values are decimal-string cents and priceCents is a reporting-only HALF_UP projection. - Use all or expired to discover invalid/old claims. Legacy basis inference is labeled. unresolved_value, invalid_window, overlap, legacy_basis_inferred, and value_basis_conflict warnings make a row or cohort unsafe for automatic ranking. - POST requires explicit decimal cents, currency, basis, baseUom, effective dates, full lane coordinates, and expectedAffectedEntryIds. FIXED accepts per_package or per_base_uom and never accepts referencePackaging. For per_base_uom, ShelfCycle multiplies by the authoritative Product quantity with exact Decimal arithmetic, stores the result per package, and returns additive writeValue evidence containing submitted price, stored price, operation, and quantity. VARIABLE accepts only per_base_uom and requires an active same-UOM reference package. Public writes accept only the org base currency. baseUom must exactly match the Product base UOM; no density, weight/volume, cross-UOM, or FX conversion occurs. - Dry-run writes nothing and requires the stable Idempotency-Key that execution will reuse. Review expectedAffectedEntryIds and expectedPlanToken, then execute the unchanged request with both. First commit returns 201; a same-key replay returns 200 with the original receipt. stale_price_book_plan means rerun GET and dry-run before human/policy review. - If a transport failure leaves the outcome unknown, retry the exact normalized payload with the same Idempotency-Key. A replayed receipt proves the original commit; a normal first-commit response proves the earlier attempt did not commit. Never change the payload under the same key or guess by selecting a visible winner. Cost Book entries: - GET /api/v1/cost-book-entries defaults to view=current and returns authoritative complete entries as exact decimal-string cents per product base UOM in the org base currency. Use productId, supplierId, and shipFromLocationId for an exact location-qualified lane; pass shipFromLocationId=null for the supplier's general fallback cost, or omit it to list every location. view=history requires productId and supplierId and can be narrowed to one location. Never turn missing or unresolved totals into zero. - Direct POST requires a complete operational cost: basis, product base UOM, and either the legacy scalar costPerBaseUomCents or a complete ordered rates[] schedule, plus terms and explicit arrangementId/newArrangement intent. Resolve foreign-currency, denominator-mismatched, incomplete, or otherwise ambiguous observations with the user before calling ShelfCycle; the Cost Book API rejects them and never stores a zero-rate direct cost. - PATCH /api/v1/cost-book-entries/{id} requires expectedCurrentEntryId and creates a corrected replacement in the same commercial lane and effective window. The original event, rate or component details, verification, timestamps, and source details remain in history. Use the same Idempotency-Key for an uncertain retry. - DELETE /api/v1/cost-book-entries/{id} requires expectedCurrentEntryId and Idempotency-Key. It removes a mistaken Cost Entry of any shape (direct, Landed breakdown or Built from) with its corrections from the live Cost Book and History, and restores the newest preceding version with the removed version's end date. It is refused while a later version replaced it, another product is built from it, a restored window would overlap another live version, or it is the last cost behind the Current Source. dryRun=true writes nothing and returns removed, restored, addedCostCount, survivingBasePrice and a confirmation sentence. Saved Price Book decisions keep their figures. PATCH of a deleted id is 404. - Landed and Built from entries support every Arrangement kind. A Built from entry keeps the exact bulk Arrangement selected for it; when several current bulk Arrangements are available, send the intended arrangementId. Freight added to a delivered price returns the delivered_freight_caution advisory. When an Arrangement name already exists, 409 cost_book_conflict returns its arrangementId. A Built from entry waiting for a current starting price has a null total and a waitingReason. - POST /api/v1/cost-book-entries/batch accepts 1 to 50 unique clientItemId items and no more than 500 total rates or components in a 2 MB body. Duplicate commercial lanes are rejected before any item is saved. Each known failure is reported for its item; retry an interrupted request with the identical body and Idempotency-Key so only pending items continue. Rate limits count submitted items and report X-RateLimit-Cost; larger approved sets can be divided into requests of at most 50 items. - A direct entry is an entered starting cost with no adders. A constructed entry has an entered starting cost plus one to fifty ordered additional costs, or an exact referenced Cost Book product plus zero to fifty adders. Origins manual, integration, agent, and import describe claim provenance; starting-evidence provenance is separate. - For machine-extracted PDF, email, CSV, or voice candidates, resolve exact product/supplier ids first with search:read. Stop on multiple matches, fuzzy-only matches, packaging/UOM ambiguity, non-base-currency quotes, or any conversion not proven from product detail qty, packagingType, packaging.unitOfMeasure, and netUnitWeightLbs. Never send names or CAS to the Cost Book endpoint. - Fetch the exact supplier-and-location lane at the intended effective date and set required expectedCurrentEntryId to the authoritative id, or null only when matchStatus=missing. Omitting shipFromLocationId writes the unspecified-location slot; it does not supersede a located row. The unspecified-location entry is the supplier's general fallback when no exact origin matches. Dry-run each candidate, present the raw quote, exact conversion formula/catalog inputs, normalized construction, current-to-proposed change, and warnings, and obtain explicit upstream confirmation for machine-extracted candidates. - Use GET /api/v1/products/{productId}/cost-context?supplierId={supplierId}&asOf=YYYY-MM-DD for one explicit active product/supplier lane. supplierId is optional: without it, fixed-supplier products follow their fixed supplier, configured variable products follow persisted Current Source, and a single eligible lane may be selected as only_available. Multiple eligible unconfigured lanes return cost_source_ambiguous with candidates; unavailable configured authority returns current_cost_source_unavailable. The response includes sourceSelection metadata, current and previous Cost Book truth, completed-purchase material and landed evidence, pending open-PO material evidence, the observation timestamp, and expectedCurrentEntryId. Purchase evidence is labeled context, not authoritative Cost Book truth; missing evidence is null, never zero. - GET /api/v1/products/{productId}/current-cost-source is the exact read-before-write path. PUT accepts supplierId, shipFromLocationId, expectedCurrentSupplierId, and expectedCurrentShipFromLocationId; location values are UUID or null and the expected tuple provides optimistic concurrency. Set supplierId to null and shipFromLocationId to null to clear. A selected supplier/location lane must remain eligible. Fixed and archived products reject writes. - PUT /api/v1/products/{productId}/current-cost-source?dryRun=true writes nothing and returns desired and previous supplier/location ids, consequence, eligibility, and verification. Execute returns updated or unchanged plus the previous location, exact current authority, and selection provenance. Clearing authority never deletes the ProductSupplier association or Cost Book history. - Compare only fields whose currencyCode, base UOM, comparisonBasis, and time relation permit it. Material and landed evidence are separate. not_comparable_currency, not_comparable_uom, not_comparable_basis, not_comparable_unclassified_basis, and not_comparable_time_basis mean no delta should be inferred. percentage_unavailable_zero_evidence permits an amount delta but no percentage. - Execute accepted rows independently with a stable source-derived Idempotency-Key and sourceRef. Every Public write records origin integration plus the authenticated actor/time. Preserve each receipt before moving on. Retry the same key only for the same normalized payload; a stale lane requires a fresh GET, review, payload, and key. - POST dryRun=true writes nothing and may omit Idempotency-Key, in which case readiness is blocked with idempotency_key_required. Its impactAnalysis includes advisory exact-lane cost context and proposed comparisons when available; cost_context_unavailable is non-blocking, and stale_context_observation means planner readiness/currentEntry remains authoritative. Execute never depends on the advisory evidence. Execute requires the header. First commit returns 201 and full Cost Book readback; same-key replay returns 200 with the same opaque claim id and original resultAction. The receipt is also the verification fallback when the key lacks read scope. - Cost Book is not Price Book, purchase history, last paid cost, a simulation/proposal engine, a fuzzy matcher, or a public batch import. Do not substitute purchase-summary or PO costs for Cost Book truth. Active company reference directory: - Use reference directory rows as safe matching hints only. Each row has stable id, displayName, externalId, alternateNames, website host, emailDomains, phone, city/state/country, status active, updatedAt, and url. - The directory is active-only. Omit includeArchived or pass includeArchived=false. includeArchived=true is unsupported. - Incremental reference refresh is not supported. updatedSince and updatedAtFrom are unsupported on reference routes; periodically repage the active directory instead. - Always fetch exact customer/supplier detail by id before writes or business decisions. A reference row is not a complete customer/supplier profile. Opportunity management: - GET /api/v1/opportunities supports title/description query, status, customerId, supplierId, assignedToId, productId, and half-open created timestamp filters. The closed sort contract is sortBy=updatedAt with sortDirection asc or desc (default desc); use asc for stalest-first focus. Equal timestamps use id ascending as the deterministic tie-breaker. Follow nextCursor by itself; the signed cursor preserves its original filters, sort, and limit. Estimates are exact decimal strings labeled with immutable estimatedValue.currencyCode and kind estimate. They are not probability-weighted, converted, revenue, margin, forecast, or accounting truth. - POST /api/v1/opportunities requires Idempotency-Key and stamps source PUBLIC_API plus the authenticated API key id. The identity is a durable receipt: replay after an internal hard delete returns idempotency_target_deleted and never recreates the record. The server stamps the organization base currency as immutable estimate denomination. - PATCH /api/v1/opportunities/{id} accepts title, description, estimatedValue, assignedToId, and status LEAD or IN_PROGRESS only. Send the latest updatedAt in If-Match. Closed records are read-only and cannot be reopened. Use POST /{id}/close with WON or LOST; WON atomically preserves existing prospect-customer promotion and cross-organization outbox behavior. PUT /{id}/meta upserts one key/value. - Detail timeline rows contain only eventType and createdAt. eventDetail is intentionally never exposed because it can contain private text. timelineCoverage bounded means only the newest 100 typed events are available. statusIntegrity legacy_closed_without_outcome reports a legacy incoherent state without coercion. Reports: - GET /api/v1/reports returns catalog rows for sales-summary, purchase-summary, open-sales-orders, ar-aging, and pipeline-summary. allowedForThisKey is computed from the superadmin-issued key's reports:read scope plus the key actor's current ShelfCycle access. If false, do not run that report with the same key. - sales-summary params: groupBy is one or two of customer, productFamily, productCode, salesOrder, salesPerson, supplier, createdBy, month. startDate and endDate are YYYY-MM-DD in org timezone. dateField is invoiceDate, shipDate, orderDate, or enteredDate. includeStatuses defaults to all four sales statuses for enteredDate and to SHIPPED and DELIVERED for other date fields; DELIVERED expands to COMPLETED. Explicit comparisonStartDate and comparisonEndDate are required together, each window is at most 366 days, the combined windows are at most 732 days, and comparison is not supported with month. comparisonClassification is new, lost, or retained; sortBy and sortDirection rank the complete filtered population before pagination. Month rows use bucketKey and groupName YYYY-MM by requested dateField with groupId null. salesOrder rows use the order UUID for groupId and bucketKey and the order number for groupName; drill through with GET /api/v1/orders/{groupId}. Two-level rows are flat combinations, not nested. - sales-summary money fields are cents-as-string in org base currency. salesCents, productCostCents, and orderCostsCents are rounded primitive measures. From v1.29.2, orderCostsCents is net of recovered pass-through: product rows carry residual fulfillment burden after mapped charge recovery; charge rows carry consumed recovery for their class (unmapped charges carry 0). Classification is governed by live fulfillment mappings; unmapped costs remain absorbed onto product rows. totalCostCents = productCostCents + orderCostsCents and gpCents = salesCents - totalCostCents. gpPct is computed from returned cents and is null when salesCents <= 0. Invoice-date windows attribute each leg when it invoices, so a netted pair can split across periods. Row-grain product margin and aggregate GP % of invoice total use different denominators. - sales-summary unitPrice is present only when productCode is one of the groupBy dimensions and the group has a uniform UOM. Non-product rows are included as the "(Non-Product)" bucket where applicable. - purchase-summary params: groupBy is supplier, productCode, productFamily, or createdBy. startDate/endDate are PO created-date basis. includeStatuses defaults to CONFIRMED and COMPLETED. spendCents sums material PO item amounts only through the registered base-currency source, using identity amounts for same-currency organizations and converted base amounts for FX organizations; missing conversion evidence fails closed. It excludes landed costs, standalone PO cost rows, AP cash payments, qty, and unit-cost measures. Comparison windows, classification, sorting, totals, and pagination follow the same governed contract as sales-summary. - open-sales-orders params: groupBy is status, customer, or createdBy. includeDraft defaults false. Population is current sales orders in CONFIRMED and SHIPPED, plus DRAFT when requested. openValueCents is line sell value of undelivered orders; cost and margin are intentionally excluded in v1. - ar-aging is current transaction-currency subledger A/R at customer × transactionCurrencyCode grain. Optional customerId and transactionCurrencyCode filters apply before full aggregation. sortBy is netCents, pastDueInvoiceCents, days90PlusCents, or oldestDaysPastDue; sortDirection defaults to desc. Ordering keeps transactionCurrencyCode lanes ascending, then applies the requested measure within each lane, with customerId as the stable tie-breaker and null oldestDaysPastDue always last. - ar-aging invoice buckets are currentCents, days1To30Cents, days31To60Cents, days61To90Cents, days90PlusCents, and unknownCents; days90PlusCents is the non-overlapping 91+ bucket (strictly more than 90 days overdue). Credits never enter those buckets: unappliedCreditCents is negative and separate. The buckets hold invoices with a positive open balance, so per row openInvoiceCents = bucket sum + any negative invoice balances (equal when the row has none); openInvoiceCents + unappliedCreditCents = netCents; the four overdue buckets sum to pastDueInvoiceCents. unappliedCreditCents, netCents, and populationDiagnostics.creditIncoherentCount are null when the key actor lacks the live credit-note permission. The server also owns pastDueInvoiceCount, unknownDueInvoiceCount, oldestDueDate, and oldestDaysPastDue; do not recompute them from timestamps or net credits against overdue invoices. populationDiagnostics counts inconsistent source records: incoherentRecordCount = negativeDueCount + paidWithDueCount + creditIncoherentCount + zeroPaidPartialCount, with postedZeroDueCount reported beside it. Lane totals are per transaction currency; the base totals use stored base amounts. Never add currencies, infer a converted winner, or treat this operational report as a GL/base-currency control balance. - pipeline-summary groups the authorized opportunity population by status, assignedTo, aspect, or createdBy. Optional status, aspect, assignedToId, createdFrom, and createdTo filters apply before aggregation. Measures are opportunityCount, exact estimatedValueTotal, noValueCount, wonCount, lostCount, and oldestOpenCreatedAt. Rows without estimates are excluded from sums and counted by noValueCount. One organization produces one explicit estimate currency; mixed source currency fails closed. Never call this weighted pipeline, probability, forecast, revenue, or accounting truth. - Unknown report keys return error.code unknown_report with validValues.reportKey. Reports can return comparison_window_too_large, comparison_not_supported_with_month, comparison_source_too_large, window_too_large, missing_permission, or report_source_data_issue. Stop and repair the request; do not retry identical malformed requests. Write readiness: - dryRun=true is supported on the documented write routes, including Product and Product-family writes, product packaging create, Product supplier/alias/customer-part-number child writes, Product lifecycle, documents, SDS, and Current Source. It requires the same write scope, current ShelfCycle access permission, and endpoint's rate limit as execution. - dryRun must be exactly dryRun=true. Invalid values such as dryRun=false, dryRun=1, or repeated dryRun parameters fail validation and must not be retried as execution. - A dryRun response has data.type write_readiness, data.operation, data.status ready or blocked, wouldWrite false, checks, duplicateCandidates, submit, verification, optional impactAnalysis, and requestId. - data.status controls readiness. HTTP 200 with data.status blocked means inspect checks before deciding whether policy allows execution. - POST dryRun reports missing Idempotency-Key, idempotency replay, idempotency payload reuse, or orphan idempotency target state without creating or deleting ExternalIdentity rows. - PATCH dryRun reports missing or stale If-Match and returns wouldChange.fieldNames plus submitted next public values without hidden before-values. - Duplicate candidates are bounded; similarity matches block unless an eligible create-distinct request explicitly permits them. If the key cannot read the matching detail or document route, candidates are redacted to readable false with only type and matchReasons. Only the six account CREATE families documented above accept explicit create-distinct intent; there is no bypass token. - Verification is scope-aware. If verification.available is true, hydrate verification.path after execute. If verification.available is false, verification.fallback is execute_response; use the execute response as readback and do not assume the key can GET detail. - Lifecycle POSTs include /products/{id}/{archive|unarchive} alongside the customer, supplier, contact, customer-address, and supplier-location routes. Product lifecycle requires Product delete permission and archive is blocked while stock or inbound quantity remains. Execute requires current updatedAt in If-Match; dryRun=true may omit it and returns blocked readiness plus currentUpdatedAt in impactAnalysis. A current-token no-op returns changed false without rewriting provenance. Lifecycle never cascades or propagates cross-org. - Customer address creates and updates must leave the address marked as billing or shipping. Customer location create auto-marks the new location primary only when no active primary location exists; clients cannot request primary switching. - Product create uses existing productFamilyId and active packagingId and may atomically assign typed Product code attributes. supplierId is allowed only when supplierType is FIXED and the key can verify supplier visibility through suppliers:read. VARIABLE packaging requires qty exactly 1. Product create mutates no inventory, orders, prices, costs, lots, documents, or accounting records. - Product PATCH requires the latest updatedAt value in If-Match and may update the documented Product fields. A unique product code can be renamed while connected sales, purchasing, inventory, pricing, cost, and accounting history stays attached to the same product. Other structural product changes continue to follow their existing readiness checks. - Product-family POST/PATCH supports name, externalId, CAS, molecular formula, UN numbers, GHS signal word, and hazard symbols only. Product-family create requires Idempotency-Key. Product-family PATCH requires latest updatedAt as If-Match and returns impactAnalysis.attachedProductCount. It never creates products, suppliers, packaging, product family attributes, SDS/documents, superfund/tax fields, inventory, costs, prices, orders, or accounting records. - Product catalog setup can create packaging with POST /product-packaging and read it through GET /product-packaging/{id}. The packaging directory supports exact-name lookup and cursor continuation. Resolve typed definitions through /product-code-attributes and /product-code-attributes/{attributeId}/values. Product create and an update that includes attributeAssignments return the accepted assignment set, including to write-only keys. An update without assignments leaves the current set unchanged, and a retried create returns the originally accepted set. Review saved assignments, supplier relationships, aliases, and customer part numbers through the product-specific lists. - Repeating the same supplier relationship or customer part number leaves the saved relationship unchanged and requires the latest product timestamp in If-Match. Alias and packaging creates use Idempotency-Key for safe retries. An alias keeps the owner and context selected when it was created. After owner permission, archive-state, and version checks pass, an alias DELETE whose aliasId is not attached to the requested owner returns 200 changed:false with the owner version unchanged; missing or stale If-Match remains blocked. This does not reveal whether that ID exists under another owner. Supplier-context writes require suppliers:read; customer-context and customer-part-number writes require customers:read. Ordinary supplier links never select Current Source, and the selected Current Source link cannot be removed. Attached documents: - To upload one private file, compute the file's lowercase hexadecimal SHA-256, then POST /api/v1/document-uploads with fileName, canonical contentType, sizeBytes, and checksumSha256. The file must be 1 byte through 15 MiB. Allowed general attachments are PDF, JPG/JPEG, PNG, GIF, WebP, BMP, DOCX, XLSX, and PPTX. Dedicated Product SDS accepts PDF and raster only. ZIP, legacy Office, macro-enabled Office, SVG, HTML, executable, unknown, extension/MIME-mismatched, and oversized files are rejected. - Send PUT directly to data.upload.url with every data.upload.headers entry verbatim, including Content-Length, Content-Type, If-None-Match: *, and x-amz-checksum-sha256. Do not send the ShelfCycle bearer key to S3. A successful direct PUT is an S3 response, not a ShelfCycle JSON envelope. A replayed PUT to the same create-only grant returns raw S3 412; request a new grant when the bytes were not durably recorded. - The upload grant writes no Product, Customer, Supplier, Purchase Order, or Document row. Associate data.document through POST /api/v1/{products,customers,suppliers,purchase-orders}/{id}/documents with the target write scope, current ShelfCycle access, and a stable Idempotency-Key. The association key may differ from the grant-minting key; documents:upload is not re-required. A new API-upload association verifies object existence, actual size, canonical content type, and the grant-bound checksum before writing. Execute returns required data.integrity: verified contains the matched checksumSha256, sizeBytes, contentType, and durable verifiedAt; an exact replay returns the same stored receipt without another storage HEAD. verified is association-time byte evidence, not malware scanning, content interpretation, perpetual-retention proof, or proof that an object cannot later change. External or legacy links return not_verified/external_or_legacy_link. A pre-v1.29.1 API-upload replay without a valid stored receipt returns unavailable/legacy_receipt_not_recorded and never invents history or depends on storage. dryRun retains the transient upload_verified check but persists no receipt; execute revalidates. A key without the target read scope/live view permission still receives integrity but receives access.mode unavailable and no URL. upload_not_found is retryable after completing the PUT; upload_verification_unavailable is retryable without changing the request. - Assign one current Product SDS with PUT /api/v1/products/{id}/sds using data.document, products:write, live Product update permission, and the latest Product updatedAt as If-Match. The assigning key may differ from the grant-minting key; documents:upload is not re-required. Exact converged retries return idempotencyStatus replayed without depending on S3. A missing sidebar Document is repaired after verification; a replacement retains prior Document rows. Same URL with a different fileName is a change. Office cannot be the dedicated SDS. The response never returns the durable object URL. - Product/SDS keys normally need documents:upload + products:write + products:read, or search:read instead of products:read, plus live Product view/update RBAC. Core record attachments normally need documents:upload + the parent write scope + the parent read scope, plus matching current ShelfCycle access. Upload authority alone never grants record association or read access. - GET /api/v1/products/{id}/documents, /customers/{id}/documents, /suppliers/{id}/documents, and /purchase-orders/{id}/documents return document links attached to one visible parent. These are not sales-order or purchase-order commercial documents. - Product document responses include data.sds when Product.sdsFile is present. The SDS entry may share a URL with a document row; treat sds as the product SDS pointer and documents[] as the audit trail. - ShelfCycle bucket rows return access.mode signed with a 15-minute URL and expiresAt. External https rows return access.mode raw. If a stored ShelfCycle bucket key is not under the authenticated org prefix, access.mode is unavailable and no URL is returned. - POST /{products,customers,suppliers,purchase-orders}/{id}/documents accepts only fileName and url. URLs must be https and cannot contain embedded credentials. External https links are allowed; ShelfCycle bucket links must belong to the authenticated org. Execute requests require Idempotency-Key and exact duplicate URLs on the same parent are blocked. - Archived parents remain readable through document GET routes, but document create on archived parents is blocked. - Documents are user-attached files and may contain tax, commercial, or quality context. Use the narrowest parent route and do not log signed URLs or sensitive request bodies. Agent recipes: - ACCOUNT_COMMERCIAL_IMPORT: call /me and inspect the served OpenAPI; resolve or create source-identified payment terms, kind-qualified representatives, and Chart of Accounts records before profile writes. Chart of Accounts create supports name, account type, source ID, category, report inclusion, and income-statement classification. Updates can rename accounts and change account type or classification while the account has no financial activity or connected assignments. Use source externalId values separately from request Idempotency-Key values. Preserve legitimate source records: use top-level duplicateResolution=create_distinct only when a supported account create deliberately represents a separate record. A dry run can show which similarities can be accepted but is not required before execution. Create customers with complete primary-location and address details and suppliers with commercial profile fields, then create source-identified contacts and notes. Record every created, replayed, blocked, and failed outcome; resume unknown outcomes with the same payload and key. Find records again by externalId, use returned updatedAt values for later PATCH requests, honor each endpoint's rate limit and Retry-After, and verify saved fields and unresolved records rather than relying on created-record counts. Stop on exact identity conflicts, hidden or incomplete candidate results, ambiguous matching, changed reference values, unsupported source fields, missing references, currency mismatch, or incomplete source details. - NEW_PRODUCT_FROM_SDS: resolve or create the Product Family, resolve packaging and optional supplier references, dry-run/create the Product, optionally PATCH supported hazmat fields with latest If-Match, grant and PUT the SDS, GET the latest Product, PUT /products/{id}/sds with its updatedAt, then GET the Product and documents for exact verification. - HISTORICAL_SDS_BACKFILL: map every local file to one Product id and classify it before writing as historical_sidebar or current_sds. For each file, grant, PUT, POST it to the Product documents route, and verify it. Only the one explicitly selected authoritative file also uses Product SDS PUT. Use bounded client concurrency of 3-5, honor Retry-After, and persist a resumable success/failure manifest; loop order never chooses the current SDS. - CORE_RECORD_ATTACHMENT_IMPORT: resolve one exact Product, Customer, Supplier, or Purchase Order, grant and PUT each allowed file, POST data.document to that record's document route with a stable source-derived Idempotency-Key, then GET the same parent document route to verify the new row. Stop on ambiguous matching, cross-org URLs, archived parents, typed policy errors, or missing live permission. - PURCHASE_EXECUTION_REFRESH: GET /api/v1/purchase-order-execution with exact filters, including optional warehouseId on the destination warehouse. Follow each nextCursor by itself, and periodically restart from the original filters for a full current-state refresh. executionChangedAt is max(PurchaseOrder.updatedAt, PurchaseOrderItem.updatedAt, included LotReceipt.updatedAt); it may advance for a cost or other non-projected row update. It excludes document and referenced supplier/product/packaging label changes, is not a deletion watermark, and may move backward after receipt deletion. Use it as a conservative refetch hint, then compare current state. Do not infer lossless child-change completeness, deletion discovery, or snapshot isolation across requests. Use receiptEventsUrl for raw evidence and documentsUrl for PO-level documents under the same authority. A warehouse-keyed comparison with another endpoint is a comparison of separately observed reads, not a certified composition. - INVENTORY_BALANCE_REFRESH: GET /api/v1/inventory-balances with optional productId, productFamilyId, and warehouseId filters. Follow each nextCursor by itself, and periodically restart from the original filters for a full current-state refresh. Do not infer snapshot isolation across requests or add quantities across products. - REORDER_RISK_REFRESH: GET /api/v1/reorder-status with exact entity/status filters and continue each nextCursor by itself. For a complete Assistant result, present largest risks by sorting projectedDeficit descending through the presentation layer; never add a Public API sort parameter or infer canonical ordering from that view. If the result is bounded, say the sort covers returned rows only. Explain each row with reorderPoint, onHand, allocated, committed, projectedAvailable, and projectedDeficit. Never total projectedDeficit across rows or quantity bases, and never treat it as an order quantity. - PURCHASE_ORDER_EVIDENCE: POST /api/v1/document-uploads, PUT the exact bytes directly with every returned header, POST data.document to /api/v1/purchase-orders/{id}/documents?dryRun=true with a stable Idempotency-Key, execute the same association without dryRun after policy approval, then GET the same parent document route for readback. Upload authority, association authority, and read authority remain independent. Commercial documents: - Finder responses for sales orders and purchase orders contain authorized canonical summaries, each document's exact total with resolved currency, stable cursor metadata, and no population aggregates. - Finder continuation uses nextCursor by itself. Changed traversal fixes snapshotAt across every page; overlap the next polling window and deduplicate by id plus updatedAt so post-snapshot updates appear in the next poll. - Sales-order detail returns a stable line page, canonical shipping instructions, explicit allocation/consumption evidence, stored totals, and signed line continuation. - Purchase-order detail returns a stable line page, supplier shipping instructions, explicit receipt evidence, classified supplier document costs with each cost's resolved ISO currency, exact reconciled totals, and signed line continuation. - GET /api/v1/purchase-order-execution returns open confirmed purchase-order lines by default; includeDraft=true adds draft lines. Optional warehouseId, supplierId, productId, productFamilyId, and paired dueFrom/dueTo filters are exact. warehouseId filters on the purchase-order destination warehouse and does not match a null warehouse header. Each row includes nullable warehouse identity, receiptEventsUrl, documentsUrl, the compatibility field updatedAt for the line, and receipt-aware executionChangedAt with the explicit boundary described above. Ordering is dueAt ascending with nulls last, then purchase-order id and line id ascending. - GET /api/v1/inventory-balances returns current supported non-reserved Lot balances at product and physical-warehouse grain. FIXED rows use package-count basis; VARIABLE rows use base-UOM basis. Ordering is product id then warehouse id. Continue with nextCursor alone. - Receipt state is exactly no_receipt_evidence, planned_not_received, quantity_basis_unknown, partially_received, received_to_expected, or over_received. Quantities are normalized decimal strings. Fixed packaging uses receipt snapshots with a labeled current-catalog fallback for legacy rows; variable packaging requires stored compatible packaging and capacity snapshots. When basis is unknown, aggregate quantities are null and raw events remain available from GET /purchase-order-lines/{id}/receipt-events. - GET/POST /api/v1/purchase-orders/{id}/documents exposes all existing core Document evidence for that purchase order. POST supports dryRun and idempotent execution; document storage upload authority remains separate. - Do not reconcile the PO costs array against totals.costsAmountCents. They answer different questions and neither contains the other. The costs array lists costs billed by that purchase order's own supplier, whatever their line linkage or margin participation. totals.costsAmountCents is the document's non-item total component: it uses stored standalone PO costs for ordinary documents and, when row-level comparable evidence is required, includes margin-participating PO cost contributors plus margin-participating linked sales-order costs billed by the PO supplier. itemsAmountCents + costsAmountCents always equals totalAmountCents. A listed cost may still be absent from the total, and the total may include costs that are not listed. Use the array for itemized supplier cost evidence, and never derive their difference as a supplier, an AP record, a bill, an invoice, a GL entry, or private cost history. - Missing, foreign, and unauthorized order ids share the same not-found response. Order payloads do not expose profit, margin, COGS, economics formulas, per-customer population totals, operational mutation seams, or OrderShipment state. Hard boundaries: - No broad ERP collection sync, webhooks, arbitrary metadata dump, order mutation, purchase-order mutation, product attributes, catalog attributes, product hierarchy mutation beyond the bounded product-family fields above, inventory mutation, lot mutation, receiving, valuation, server-side bulk/folder/ZIP processing, broad document/file sync, document deletion, broad row-level invoice/credit/bill/payment or AP/GL access, cost cascade internals, general duplicate bypass, or shipment-platform API. Duplicate admission is limited to the six documented CREATE families and never bypasses externalId or database uniqueness. AR access is limited to the governed reports documented by GET /api/v1/reports. Cursor pagination is limited to documented endpoints. - Product.name is not a public field. Product display uses code plus product family and packaging facts. - Money fields are string cents. Quantities are decimal strings. Do not convert cents to JS floating dollars for comparisons. - Order quantities are decimal strings and order money is exact cents paired with an explicit ISO currency. Do not sum finder rows across currencies. - For sales-order and purchase-order changed-record discovery, use changedFrom and changedTo as a half-open parent updatedAt window. Continue with nextCursor alone while hasMore is true, then overlap the next poll and deduplicate by id plus updatedAt. - fromDate and toDate are organization-local calendar days, and the date they filter on depends on the resource: sales orders use the ordered date, purchase orders use the due date. Use changedFrom and changedTo instead when you want records that were edited in a window rather than dated in one. Error handling: - Keep requestId from every response. - Validation errors may include repairCategory, suggestedAction, acceptedFilters, validValues, requiredCombination, maxWindowDays, documentationPath, example, doNotRetrySameRequest, writeReadiness, and impactAnalysis. Treat these as machine-readable repair instructions, not loose hints that relax the schema. - Optimistic concurrency failures come back as HTTP 409 with error.code stale_record, not 412. The version token is the record's latest updatedAt, sent as If-Match. On stale_record, GET the record again, rebuild the request from the fresh updatedAt, and retry once. - If /search rejects type, replace it with types. The API does not accept type as an alias. If /contacts rejects a broad list request, provide parentType plus parentId. If an order cursor fails, stop and restart from the original normalized filters; never combine cursor or lineCursor with other query parameters. - Stop on HTTP error.code invalid_api_key, api_key_revoked, api_key_expired, api_actor_inactive, missing_scope, missing_permission, stale_record, stale_current_cost_source, current_cost_source_unavailable, cost_source_ambiguous, fixed_product_source_authority, archived_record, finder_filter_required, unsupported_sync_filter, duplicate_customer, duplicate_supplier, duplicate_product, duplicate_family, duplicate_contact, duplicate_customer_location, duplicate_customer_address, duplicate_supplier_ship_from_location, duplicate_document, document_url_invalid, document_url_cross_org, unsupported_document_field, unsupported_document_type, document_too_large, document_checksum_invalid, upload_integrity_invalid, blocked_order_sensitive_update, primary_change_unsupported, address_role_required, idempotency_key_required, idempotency_key_reused, invalid_variable_qty, invalid_packages_per_pallet, invalid_reference, unsupported_product_field, unsupported_family_field, or reference_visibility_blocked. On stale_current_cost_source, GET the exact Current Source path and re-evaluate the desired transition. On cost_source_ambiguous, choose one returned supplierId only under explicit upstream policy. Retry upload_not_found only after completing the direct PUT; retry upload_verification_unavailable without changing the association request. - On dryRun HTTP 200 with data.status blocked, inspect blocked checks such as duplicate_candidate_found, idempotency_target_missing, idempotency_key_required, idempotency_key_reused, stale_record, archived_record, target_not_found_or_not_visible, product_family_reference_active, product_packaging_reference_active, supplier_reference_active, attribute_assignments_invalid, base_uom_transition_blocked, selected_supplier_link, reference_visibility_blocked, variable_qty_valid, order_sensitive_update_blocked, or validation_failed before deciding next action. Product execution can also return invalid_packages_per_pallet or invalid_reference when a persisted pallet rule or referenced record blocks the write; correct the request or refresh referenced IDs before retrying. - Authenticated requests that pass scope authorization and reach the limiter return X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset, where reset is Unix epoch seconds for the active API-key/endpoint/family window. A 429 retains all three, remaining is 0, and Retry-After is a positive delay consistent with that window. Requests rejected before a usable limiter result do not expose per-key window state. These bearer-key routes remain server-to-server; no cross-origin browser availability is promised. - On 429, respect Retry-After and retryAfterSeconds, wait before retrying, and inspect any prior 422 doNotRetrySameRequest errors before sending more traffic. Never treat a rate limit as proof that a record is absent. - 404 may mean missing, cross-org, unsupported, or not visible to the acting user. Do not infer existence from failures. Safe prompt for local agents: Read SHELFCYCLE_API_KEY from .env and never print it. Call GET /api/v1/me, parse apiContractVersion v1.47.0 and minimumCompatibleContractVersion v1.21 as numeric tuples using the compatibility algorithm above, then verify organization base currency, org, actor, key preview, scopes, documentation links, effective capabilities, reports availability, writeReadiness, and required OpenAPI operationIds. For account onboarding, resolve kind-qualified representatives, payment terms, and eligible ledger accounts first; create or correct supported Chart of Accounts records only through the explicit ledger-account routes; keep source externalId separate from Idempotency-Key; preserve legitimate source records; record every outcome; resume with identical keys and payloads; reuse updatedAt for PATCH; and verify exact field coverage. For stock-out counts, use inventory-balances census products.noneAvailableInScope and pairs.zeroAvailable with their exact product and product/warehouse-position nouns; never recount a page. Inventory-balances and reorder-status have no lot receipt-age; use GET /api/v1/lots for named lots, per-row qty/basis, and remaining-physical inventory-addition age, then GET /api/v1/lots/{id} with an exact UUID. Never sum unlike lots. For reorder questions, use reorder-status statusCounts, explain each row with allocated, committed, projectedAvailable, and projectedDeficit, and always disclose configuredCoverage plus the exclusion of inbound/on-order quantities. projectedDeficit is a same-row threshold gap, never an order quantity; do not total it across rows or quantity bases. For certification questions, use certification-types, certification-records, and certification-subjects as the structured factual source. Inventory an exact subject without a type/text restriction before making a type-absence statement. Results describe stored ShelfCycle records, not external certification or compliance truth; evidence exposes recordedState and fileName metadata only and never a URL, download, availability, or content claim. Use Entity Resolution for stable IDs, Tags for exact tag vocabulary and membership, Product Directory for product/family populations, company reference directories for account setup, GET /api/v1/reports for governed report discovery, then use reports, inventory balances, reorder status, purchase execution, opportunity management, note activity, typed finders, product setup references, parent-scoped child/document lists, and exact detail endpoints after selecting an id. Read Current Source before any source-authority write; dry-run the exact supplierId, shipFromLocationId, expectedCurrentSupplierId, and expectedCurrentShipFromLocationId, review consequence and eligibility, then execute only under policy. For Price Book writes, call POST /api/v1/price-book-entries?dryRun=true with a stable Idempotency-Key, review expectedAffectedEntryIds and expectedPlanToken, then execute the same intent with those exact values; if execution returns stale_price_book_plan, repeat dry-run and human/policy review instead of guessing. Continue cursor traversal with the returned signed cursor alone. For other supported writes, optionally submit the exact intended request with dryRun=true, review data.status/checks/duplicateCandidates/impactAnalysis/verification against your configured policy, then execute without dryRun when policy allows. For customer, supplier, contact, combined customer-location, customer-address, and supplier ship-from creates, set top-level duplicateResolution=create_distinct only when a distinct source record is deliberate. A dry run is optional, and exact identity conflicts still block the request. Use stable Idempotency-Key values for create POSTs and latest updatedAt as If-Match for PATCH or Product SDS PUT. Upload evidence only through POST /document-uploads plus its direct S3 PUT, then associate the returned descriptor through the exact Product, Customer, Supplier, or Purchase Order documents route or Product SDS route. Never create/update orders, purchase orders, inventory, posting, balances, fragments, groups, ledger-account lifecycle, Cost Book amounts, GL, or catalog taxonomy definitions outside the explicit supported routes; use only the explicit packaging and typed Product-assignment routes for catalog setup. Never read or rely on Product.name. Redact the key, filenames, checksums, object URLs/keys, signed URLs/headers, and sensitive request bodies from logs. ## Warehouses and reorder-point setup (v1.39) GET /api/v1/warehouses uses warehouses:read. Search names with q or match one exact source identity with externalId. Active physical warehouses are returned by default; includeArchived=true and includeDirectShip=true add those locations when needed. Continue with the returned cursor by itself. GET /api/v1/warehouses/{id} returns one warehouse with its directory fields and updatedAt. Entity Resolution accepts class=warehouse with search:read and the acting person's current Lot viewing access. POST /api/v1/warehouses and PATCH /api/v1/warehouses/{id} accept name, street1, street2, city, country, state, zip, phone, email, contactName, and externalId. Create requires name. A repeated externalId and matching fields returns changed:false; conflicting fields identify the existing warehouse. Without externalId, supply Idempotency-Key. Updates and archive or restore actions use the latest updatedAt in If-Match. Every write accepts dryRun=true and requires warehouses:write plus current organization access. GET /api/v1/shipping-options uses shipping-options:read. It returns sales and purchase freight terms, sales shipping carriers, FOB terms, freight methods, and border crossing points. The default directory includes all six types; use type, q, externalId, or includeArchived to narrow the result. Each row includes id, type, value, externalId, archivedAt, and updatedAt. Payment terms remain available from /api/v1/payment-terms with Company Reference access. Source IDs belong to the selected Shipping Option type. POST /api/v1/shipping-options and PATCH /api/v1/shipping-options/{id} accept the documented type, value, and source ID fields. Create uses a stable Idempotency-Key for a new option. Update and archive or restore actions use the latest updatedAt value in If-Match. Every write accepts dryRun=true and requires shipping-options:write plus current organization access. POST /api/v1/reorder-points requires reorder:write plus current organization access and accepts {items:[{warehouseId,productId,reorderPoint}]}, with 1 to 50 unique product-and-warehouse positions. Integer thresholds use base units for variable products and package counts for fixed products. Zero or below clears a threshold, identical values return changed:false, and dryRun=true previews the full group. Review the saved thresholds with GET /api/v1/reorder-status using reorder:read. Supplier History: GET /api/v1/supplier-history requires both cost-book-entries:read and suppliers:read plus current Cost Book and Supplier access. Pass 1 to 25 supplierIds, and optionally narrow the result with up to 50 nameQueries or 200 productIds. Each supplier reports coverage for its selected scope, supplier-specific names, product relationships, purchase evidence, saved arrangements, and latest costs with currency, unit, and basis. GET /api/v1/product-directory accepts up to 50 nameQueries in one call, separately from nameQuery. Rows include matchedQueries, while nameQueryCounts and unmatchedQueries report per-name coverage.