Product setup
Use setup references, dry-run readiness, bounded catalog writes, and Product SDS assignment to maintain product records.
The API can create and update product records, create and update product families with supported identity and regulatory fields, upload files, and assign one uploaded PDF or image as the current Product SDS. 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. Use the Attached documents guide for file uploads, product attachments, and SDS assignment.
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. For a variable-supplier product, manage Current Source separately after eligible Cost Book context exists. - Submit the exact intended
POSTorPATCHrequest withdryRun=true. - Execute with the same body when your policy allows it. Use
Idempotency-KeyforPOSTand latestupdatedAtasIf-MatchforPATCH. - When the product needs an SDS, upload the PDF or image, then submit the returned document value to
PUT /products/{id}/sdswith the latest productupdatedAtasIf-Match.
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 a variable-supplier product, use GET and PUT /products/{id}/current-cost-source to read or manage the selected supplier and ship-from origin. This uses the separate Current Source scopes and keeps the product's supplier type unchanged. See the Cost Book guide for the complete workflow.
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. Assign sdsFile through PUT /products/{id}/sds, not inside the product create or update body.
Resolve the exact product before assigning its SDS, use the latest product timestamp for the assignment, and verify the result through the product's document list.