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

  1. Call GET /me and confirm the matching effectiveCapabilities.products, effectiveCapabilities.productFamilies, and effectiveCapabilities.writeReadiness flags.
  2. Resolve product families with GET /product-families and active packaging with GET /reference/product-packaging.
  3. If a product has a fixed supplier, verify the supplier is visible before including supplierId.
  4. Submit the exact intended POST or PATCH request with dryRun=true.
  5. Execute with the same body when your policy allows it. Use Idempotency-Key for POST and latest updatedAt as If-Match for PATCH.

Endpoint map

EndpointScopeUse it for
GET /product-familiesproducts:read, products:write, or search:readFind existing product family references by q or exact externalId.
GET /product-families/{id}products:read, products:write, or search:readRead one existing product family reference.
GET /reference/product-packagingproducts:read, products:write, or search:readList active packaging references by q or unitOfMeasure.
POST /product-familiesproducts:writeCreate one bounded product family from public-safe identity and regulatory fields.
PATCH /product-families/{id}products:writeUpdate bounded product-family fields with optimistic concurrency and impact analysis.
POST /productsproducts:writeCreate one bounded product catalog record after reference and duplicate checks.
PATCH /products/{id}products:writeUpdate 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.

Guardrail

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.