# ShelfCycle Public API v1.21 ShelfCycle's public API keeps the route major at /api/v1 and the contract version at v1.21. Call GET /api/v1/me first. It returns apiVersion "v1", apiContractVersion "v1.21", org, key preview, scopes, actor, documentation links, and coarse effective capabilities after live RBAC, including Cost Book view/create and write readiness. Capabilities do not prove a specific record can be written; use dryRun=true for target visibility, idempotency, optimistic concurrency, 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. 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. - Use GET /api/v1/search for ambiguous cross-resource resolution across customer, supplier, contact, product, sales order, purchase order, and location cards. - Use root finder endpoints for typed narrowed lookup: GET /customers, /suppliers, /products, /orders, and /purchase-orders. These are finders, not sync lists. They reject unfiltered requests, updatedSince, and standalone status filters. - Use GET /api/v1/reference/customers and GET /api/v1/reference/suppliers only to page active customer/supplier matching hints for local caches. They require company-reference:read, return active rows only, and use cursor + limit pagination. They are not full CRM exports or write authorization. - 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. - Hydrate exact resources only after selecting a specific id. Use GET /customers/{id}, /suppliers/{id}, /products/{id}, /orders/{id}, /purchase-orders/{id}, /contacts/{id}, /notes/{id}, /customers/{id}/locations/{locationId}, /customers/{id}/locations/{locationId}/addresses/{addressId}, and /suppliers/{id}/ship-from-locations/{locationId}. - 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. Note lists are metadata-only. Fetch a note body only after choosing an exact visible note. - For supported POST/PATCH writes, optionally call the same endpoint with dryRun=true before execution. dryRun checks target visibility, idempotency, duplicate candidates, optimistic concurrency, eligible child-record shape, product reference eligibility, and verification guidance without writing. The customer agent's own policy decides whether to submit; ShelfCycle does not require a human approval step. - Execute supported POST/PATCH writes without dryRun after your policy allows it, with Idempotency-Key on POST and If-Match on PATCH. Execution is authoritative and re-runs every invariant. Scopes: - search:read: GET /search, legacy detail readback for customer/supplier/contact/product detail, and exact parent-scoped child/document reads after selecting a concrete customer, supplier, or product parent. - contacts:read: GET /contacts and GET /contacts/{id}. - contacts:create: POST /contacts. - contacts:update: PATCH /contacts/{id}. - notes:read: GET /notes and human/API-created GET /notes/{id}. - notes:write: POST /notes, PATCH API-created notes, and legacy API-created GET /notes/{id}. - customers:read, suppliers:read, products:read: root finders, detail reads, and exact parent-scoped document reads for those resources. customers:read and suppliers:read also allow parent-scoped active child reads. - customers:write: bounded customer profile create/update plus parent-scoped customer location, address, and document-link create. It does not allow primary switching, credit/terms/rep defaults, archive/unarchive, ledger, tax, workflow defaults, binary upload, document deletion, or broad document sync. - suppliers:write: bounded supplier profile create/update plus parent-scoped supplier ship-from location and document-link create. It does not allow supplier root address mutation, tax, ACH, payment terms, credit, ledger, representative defaults, archive/unarchive, binary upload, document deletion, or broad document sync. - products:write: bounded product create/update, product-family create/update, and parent-scoped product document-link create when the key has the scope and the actor has matching live ERP ProductPermission. It does not allow packaging create, supplier create, product code attributes, catalog attributes, binary upload, document deletion, inventory, prices, costs, orders, lots, receiving, accounting, duplicate override, or bulk import. - orders:read: sales-order finder and read-only commercial document. - purchase-orders:read: purchase-order finder and read-only commercial document. - commercial-economics:read: scoped SO/PO commercial economics fields such as costs, totals explanations, profit, and margin where source data supports them. It only applies with the relevant order read scope and live RBAC. - company-reference:read: active customer/supplier reference directory for matching. It excludes contacts, notes, order history, prices, costs, margin, payment, tax, ledger, and raw address details. - reports:read: governed commercial report rollups through GET /api/v1/reports and GET /api/v1/reports/{reportKey}. It requires live report or purchase-order RBAC per report and excludes row-level export, raw SQL, scheduling, files, AR/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. It is included in full access and never consults the Pricing Engine feature flag. - cost-book-entries:write: POST /api/v1/cost-book-entries with live pricingEngine.manage. It is included in full access; restricted keys may receive it explicitly. A successful execute creates or supersedes verified Cost Book truth; it is not a proposal or pending approval. 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 and supplierId for an exact lane. view=history requires both ids and can return direct/constructed, authoritative/supporting/non-authoritative, superseded, archived, and unresolved rows. Never turn missing or unresolved totals into zero. - 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 lane at the intended effective date and set required expectedCurrentEntryId to the authoritative id, or null only when matchStatus=missing. 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 exact active product/supplier lane. It returns 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. - 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 in v1.21. - v1.21 has no incremental reference refresh. 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. Governed reports: - GET /api/v1/reports returns catalog rows for sales-summary, purchase-summary, and open-sales-orders. allowedForThisKey is computed from the superadmin-issued key's reports:read scope plus the key actor's live RBAC. If false, do not run that report with the same key. - sales-summary params: groupBy is one or two of customer, productFamily, productCode, salesPerson, supplier, month. startDate and endDate are YYYY-MM-DD in org timezone. dateField is invoiceDate, shipDate, or orderDate. includeStatuses defaults to SHIPPED and DELIVERED; 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. 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. totalCostCents = productCostCents + orderCostsCents and gpCents = salesCents - totalCostCents. gpPct is computed from returned cents and is null when salesCents <= 0. Shipped measure semantics are frozen; future improvements arrive as new measures or reports, not in-place redefinition. - 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, or productFamily. 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 or customer. 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. - 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 POST/PATCH /notes, /contacts, /customers, /suppliers, /customers/{id}/locations, /customers/{id}/locations/{locationId}, /customers/{id}/locations/{locationId}/addresses, /customers/{id}/locations/{locationId}/addresses/{addressId}, /suppliers/{id}/ship-from-locations, /suppliers/{id}/ship-from-locations/{locationId}, POST/PATCH /products, POST/PATCH /product-families, and POST /{products,customers,suppliers}/{id}/documents. It requires the same write scope, live RBAC permission, and rate-limit family 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 advisory and bounded. If the key cannot read the matching detail or document route, candidates are redacted to readable false with only type and matchReasons. v1.18 has no duplicate override or create-new 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. - 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; supplierId is allowed only when supplierType is FIXED and the key can verify supplier visibility through suppliers:read or search:read. VARIABLE packaging requires qty exactly 1 and execution stores qty as 1. Product create stores no productCodeAttributeValues and mutates no inventory, orders, prices, costs, lots, documents, or accounting records. - Product PATCH requires latest updatedAt as If-Match. It may update code, externalId, productFamilyId, packagingId, packagingType, qty, supplierType, supplierId, CAS, hazmat, freight, NMFC, net weight, and pallet metadata. If sales order, purchase order, lot, price book, or supplier cost usage exists, code, productFamilyId, packagingId, packagingType, qty, supplierType, and supplierId changes are blocked; externalId and non-usage-sensitive metadata may proceed with impactAnalysis. Product PATCH never mutates order, lot, price, cost, inventory, or accounting records. - 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, catalog attributes, SDS/documents, superfund/tax fields, inventory, costs, prices, orders, or accounting records. Attached documents: - GET /api/v1/products/{id}/documents, /customers/{id}/documents, and /suppliers/{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}/{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. Commercial documents: - Finder responses for sales orders and purchase orders are summaries only. They do not expose commercialEconomics. - Sales-order detail returns header, lineItems, stored totals, canonical shipping instructions, and costs. With commercial-economics:read it also returns commercialEconomics, line-item economicsParticipation and costBasis, and cost rows with explicit economicsParticipation. - If a sales-order product cost basis is unavailable, commercialEconomics.economicsStatus is incomplete_cost_basis and total cost, profit, and margin are null rather than guessed. - Purchase-order detail returns header, lineItems, stored totals, supplier shipping instructions, and supplier-owned costs. With commercial-economics:read it returns all safe PO cost rows, base and landed line amounts, totalsExplanation, documentTotalsParticipation, economicsParticipation, and counterpartyRelationship. - PO cost descriptions for other-supplier or unspecified counterparties are neutralized. Do not infer hidden suppliers, AP records, bills, invoices, GL entries, or private cost history from public PO costs. 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/lots/receiving, broad document/file sync, binary upload, document deletion, invoice/bill/payment/AP/AR/GL, cost cascade internals, duplicate override token, or shipment-platform API. Cursor pagination is limited to the documented active customer/supplier reference directory and parent-scoped child lists. - Product.name is deprecated and is never read or exposed. 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. - Fixed and variable packaging line items use the same API shape. Read packagingType, productQty, quantity, unit price/cost cents, and extendedAmountCents together. - Read participation metadata before summing money fields. A visible cost or line item may be marginable, non-margin, already represented in item cost basis, excluded from totals, or unknown. - For sales-order and purchase-order changed-record discovery, use both updatedAtFrom and updatedAtTo as a half-open parent updatedAt window. If hasMore is true, split the window before assuming the changed-record scan is complete. Error handling: - Keep requestId from every response. - v1.21 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. - 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 /orders or /purchase-orders rejects status by itself, pair status with an exact identifier or bounded date window. - Stop on HTTP error.code invalid_api_key, api_key_revoked, api_key_expired, api_actor_inactive, missing_scope, missing_permission, stale_record, 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, blocked_order_sensitive_update, primary_change_unsupported, address_role_required, idempotency_key_required, idempotency_key_reused, invalid_variable_qty, unsupported_product_field, unsupported_family_field, or reference_visibility_blocked. - 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, unsupported_profile_shape, primary_change_unsupported, address_role_required, unsupported_linked_record, product_family_reference_active, product_packaging_reference_active, supplier_reference_active, reference_visibility_blocked, variable_qty_valid, order_sensitive_update_blocked, not_api_created, or validation_failed before deciding next action. - 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 and verify apiContractVersion v1.21, org, actor, key preview, scopes, documentation links, effective capabilities, reports availability, and writeReadiness. Use the active company reference directory only for matching caches, GET /api/v1/reports for governed report discovery, then use reports, search, typed finders, product setup references, parent-scoped child/document lists, and exact detail endpoints after selecting an id. For 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. Use stable Idempotency-Key values for POST and latest updatedAt as If-Match for PATCH. Attach document links only through exact /products/{id}/documents, /customers/{id}/documents, or /suppliers/{id}/documents routes; never upload binaries or create/update orders, purchase orders, packaging, suppliers, inventory, accounting, costs, GL, product attributes, or catalog attributes through product setup. Never read or rely on Product.name. Redact the key, signed URLs, and sensitive request bodies from logs.