Product setup
Use setup references, dry-run readiness, and bounded product catalog writes to maintain safe setup records.
Product setup is intentionally narrow. The API can create and update product records, and it can create and update product families with public-safe identity and regulatory fields. Product updates that would change order-sensitive identity, packaging, or supplier semantics are blocked when the product already has sales order, purchase order, lot, price book, or supplier cost usage. Product setup does not create packaging, suppliers, prices, costs, inventory, binary uploads, document deletion, or accounting records. Use the Attached documents guide for parent-scoped product document links.
Setup flow
- Call
GET /meand confirm the matchingeffectiveCapabilities.products,effectiveCapabilities.productFamilies, andeffectiveCapabilities.writeReadinessflags. - Resolve product families with
GET /product-familiesand active packaging withGET /reference/product-packaging. - If a product has a fixed supplier, verify the supplier is visible before including
supplierId. - Submit the exact intended
POSTorPATCHrequest withdryRun=true. - Execute with the same body when your policy allows it. Use
Idempotency-KeyforPOSTand latestupdatedAtasIf-MatchforPATCH.
Endpoint map
| Endpoint | Scope | Use it for |
|---|---|---|
GET /product-families | products:read, products:write, or search:read | Find existing product family references by q or exact externalId. |
GET /product-families/{id} | products:read, products:write, or search:read | Read one existing product family reference. |
GET /reference/product-packaging | products:read, products:write, or search:read | List active packaging references by q or unitOfMeasure. |
POST /product-families | products:write | Create one bounded product family from public-safe identity and regulatory fields. |
PATCH /product-families/{id} | products:write | Update bounded product-family fields with optimistic concurrency and impact analysis. |
POST /products | products:write | Create one bounded product catalog record after reference and duplicate checks. |
PATCH /products/{id} | products:write | Update safe product identity, reference semantics, metadata, hazmat, freight, and pallet fields. |
Reference endpoints are lookup surfaces, not broad product hierarchy exports. Product-family lookup requires q or exact externalId; packaging lookup is capped and active-only. Product-family writes are bounded to name, external id, CAS, molecular formula, UN numbers, GHS signal word, and hazard symbols.
Resolve references
curl "$SHELFCYCLE_API_BASE_URL/product-families?q=Acetone&limit=10" \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY"
curl "$SHELFCYCLE_API_BASE_URL/reference/product-packaging?q=Drum&limit=10" \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY"
Use the returned productFamilyId and packagingId in product create or update requests. Do not send display names in place of ids.
Dry-run product create
curl "$SHELFCYCLE_API_BASE_URL/products?dryRun=true" \
-X POST \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: intake:doc-913:create-product" \
-d '{
"code": "IPA-55-DRUM",
"externalId": "intake-doc-913",
"productFamilyId": "product-family-id",
"packagingId": "packaging-id",
"packagingType": "FIXED",
"supplierType": "VARIABLE",
"qty": "55",
"casNumber": "67-63-0",
"unNumber": "1219",
"properShippingName": "Isopropanol",
"hazardClass": ["3"],
"packingGroup": "II",
"hazardSymbols": ["flame"],
"netUnitWeightLbs": "6.61",
"packagesPerPallet": 4
}'
{
"data": {
"type": "write_readiness",
"operation": "products.create",
"status": "ready",
"wouldWrite": false,
"checks": [
{ "code": "scope_authorized", "status": "passed" },
{ "code": "code_attribute_boundary_clear", "status": "passed" },
{ "code": "product_family_reference_active", "status": "passed", "param": "productFamilyId" },
{ "code": "product_packaging_reference_active", "status": "passed", "param": "packagingId" },
{ "code": "duplicate_check_clear", "status": "passed" }
],
"duplicateCandidates": [],
"verification": {
"available": true,
"path": null,
"pathTemplate": "/api/v1/products/{id}",
"requiresScopes": ["products:read", "search:read"],
"fallback": "detail_get"
},
"requestId": "request-id"
}
}
status: "blocked" can still return HTTP 200. Inspect checks such as product_family_reference_active, product_packaging_reference_active, supplier_reference_active, reference_visibility_blocked, variable_qty_valid, duplicate_candidate_found, and idempotency_key_required before deciding the next action.
Execute and replay
Remove dryRun=true to execute. Repeating the same route, same idempotency key, and same body returns the existing product with idempotencyStatus: "replayed".
{
"data": {
"type": "product",
"id": "product-id",
"code": "IPA-55-DRUM",
"packagingType": "FIXED",
"supplierType": "VARIABLE",
"qty": "55",
"productFamily": { "id": "product-family-id", "name": "Isopropyl Alcohol" },
"packaging": { "id": "packaging-id", "name": "Drum", "unitOfMeasure": "GALLON" },
"idempotencyStatus": "created"
}
}
Dry-run product update
Fetch the product first and send the latest updatedAt as If-Match. PATCH dry runs report the fields that would change and can include impactAnalysis when existing orders, lots, prices, or supplier costs constrain identity changes.
curl "$SHELFCYCLE_API_BASE_URL/products/product-id?dryRun=true" \
-X PATCH \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: 2026-06-30T14:35:00.000Z" \
-d '{
"externalId": "intake-doc-913-rev-b",
"freightClass": "70",
"hazardSymbols": ["flame", "health_hazard"]
}'
{
"data": {
"type": "write_readiness",
"operation": "products.update",
"status": "ready",
"wouldWrite": false,
"wouldChange": {
"fieldNames": ["externalId", "freightClass", "hazardSymbols"]
},
"impactAnalysis": {
"usageCounts": { "salesOrders": 1, "purchaseOrders": 1 },
"blockedFieldNames": [],
"allowedFieldNames": ["externalId", "freightClass", "hazardSymbols"],
"orderEconomicsMutated": false
},
"requestId": "request-id"
}
}
If the product is already used by sales orders, purchase orders, lots, price books, or supplier costs, changes to code, productFamilyId, packagingId, packagingType, qty, supplierType, and supplierId can return blocked_order_sensitive_update. Refresh the product before retrying any stale_record response.
Product-family writes
Create product families with POST /product-families and update them with PATCH /product-families/{id}. Use Idempotency-Key for create and If-Match for update.
curl "$SHELFCYCLE_API_BASE_URL/product-families?dryRun=true" \
-X POST \
-H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: intake:doc-913:create-family" \
-d '{
"name": "Isopropyl Alcohol",
"externalId": "family-ipa",
"casNumber": "67-63-0",
"molecularFormula": "C3H8O",
"unNumbers": ["1219"],
"ghsSignalWord": "Danger",
"hazardSymbols": ["flame"]
}'
Product-family PATCH can return impactAnalysis.attachedProductCount so clients know how many products are attached before changing shared family facts. It never rewrites product rows or creates products, suppliers, packaging, catalog attributes, document links, inventory, costs, prices, orders, or accounting records.
Supplier and variable rules
Only include supplierId when supplierType is FIXED. The key must be able to verify supplier visibility through suppliers:read or search:read, and the supplier must be active in the same org.
For variable packaging, send packagingType: "VARIABLE" and qty: "1". Variable packaging with any other quantity returns invalid_variable_qty, and execution stores quantity as 1.
Unsupported fields
The product write contract rejects fields outside the setup boundary, including:
displayName
name
productCodeAttributeValueIds
productCodeAttributeValues
sdsFile
documents
files
imageUrl
prices
costs
Use product family, packaging, supplier, and hazard/shipping facts that the OpenAPI schema accepts. Do not attempt to backfill fields outside the public product setup contract.
Product setup stays bounded to product and product-family setup fields. It does not create packaging or suppliers, upload files, delete documents, set product code attributes or catalog attributes, create inventory, prices, costs, orders, lots, receiving, AP, AR, GL, or bypass duplicate checks.