Skip to content

E-commerce cut-list calculator

An embedded JavaScript widget that provides a complete cut-list calculator for your e-commerce, estimation or quotation website functionality.

SmartCut Checkout allows you to:

  • Embed a cut-list calculator directly on your website
  • Calculate prices / estimates in real-time on the client side
  • Filter & sort products in your catalogue
  • Customize the UI to match your brand
  • Support multiple materials, thicknesses, dimensions, extras, and configurations
  • No server-side integration required

Include the script and default css styling in your HTML:

<link rel="stylesheet" href="https://cutlistevo.com/checkout/checkout.css" />
<script type="module" src="https://cutlistevo.com/checkout/checkout.js"></script>
<!-- Container for the calculator -->
<div id="smartcut-app"></div>
<script>
// Configuration
const initData = {
stock: [
{
l: 2440,
w: 1220,
t: 18,
cost: 65,
material: 'Plywood'
}
],
saw: {
stockType: 'sheet',
bladeWidth: 3.2,
cutType: 'efficiency',
cutPreference: 'l'
},
options: {
locale: 'en-US',
currency: 'USD'
}
}
// Wait for the ready event and then call init
window.addEventListener('smartcut/ready', () => {
window.smartcutCheckout.init(initData)
})
// Listen for calculation events
window.addEventListener('smartcut/calculating', () => {
console.log('Calculation started')
})
// Listen for results
window.addEventListener('smartcut/result', (e) => {
const result = e.detail
console.log('Optimization complete:', result)
console.log('Total stock cost:', result.checkout.formattedTotalStockCost)
})
</script>

Define the available stock materials:

const initData = {
stock: [
{
l: 2440,
w: 1220,
t: 18,
cost: 65,
material: 'Plywood'
}
],
saw: {
stockType: 'sheet',
bladeWidth: 3.2,
cutPreference: 'l'
},
options: {
locale: 'en-US',
currency: 'USD',
apiVersion: 3 // 2 or 3 (default: 3)
}
}

Stock Item Properties:

PropertyTypeDescription
lnumberSheet length in mm (required)
wnumberSheet width in mm (required)
tnumberSheet thickness in mm (required)
materialstringFriendly display name shown in the material picker (required)
codestringUnique SKU / identifier for this specific stock item (recommended — see below)
costnumberCost per sheet
weightnumberWeight in kg
descriptionstringDescription shown in the picker
imageUrlstringProduct image URL
brandstringBrand name (shown as picker hierarchy)
variantstringVariant / decor name
db_idstringYour database ID — passed through to results for lookup
fullSizeOnlybooleanWhen true, the customer cannot enter custom part sizes on this material
grain'l' | 'w' | ''Grain direction of this stock (length-aligned, width-aligned, or none)
preventGrainRotationbooleanWhen true, parts placed on this stock cannot be rotated against grain
categorystringCategory for filtering (used by Product Filtering UI)
tagsstring[]Searchable tags (used by Product Filtering UI)
availablebooleanAvailable-for-purchase flag (used by Product Filtering UI)
imageUrlstringProduct image URL — surfaced in results too
db_sawIdstringMongo ObjectId of the saw to use when this stock is picked. Lets you pin different materials to different saws per pick — see Per-Pick Saw Mapping

Stock items are grouped by their material field into a single picker entry. Multiple items with the same material but different dimensions (e.g. different thicknesses or sheet sizes) all appear under one dropdown row — the customer selects the material once, then the calculator filters available thicknesses automatically.

The code field is the per-item identifier: it distinguishes between individual stock rows that share the same material name, and it is carried through to the calculation result so you can map each used sheet back to a specific product in your catalogue.

// Two sheet sizes of the same material — one picker entry, two thickness options
stock: [
{ l: 2440, w: 1220, t: 18, cost: 65, material: 'Birch Plywood', code: 'BPW-2440-18' },
{ l: 2440, w: 1220, t: 12, cost: 48, material: 'Birch Plywood', code: 'BPW-2440-12' },
]
// Two different materials — two picker entries
stock: [
{ l: 2440, w: 1220, t: 18, cost: 65, material: 'Birch Plywood', code: 'BPW-18' },
{ l: 2440, w: 1220, t: 18, cost: 45, material: 'White MDF', code: 'MDF-18' },
]

When code is omitted, the calculator falls back to the composed label (brand + variant + material) as the grouping key — this works for simple setups but can cause items to appear as separate entries if any field differs between rows of the same material. Always supply code when passing multiple stock rows.

Pre-populate the calculator with parts at init time. Parts arrive as an array on the parts key of the init payload:

const initData = {
stock: [...],
parts: [
{
l: 600,
w: 400,
t: 18,
q: 4,
name: 'Shelf',
material: 'Plywood',
orientationLock: 'l', // 'l', 'w', or '' (free)
banding: {
sides: { l1: 'oak|1mm', l2: 'oak|1mm', w1: '', w2: '' }
},
finish: {
faces: { a: 'spray|matt', b: '' }
},
customData: { projectId: '12345', orderRef: 'ORD-001' }
}
],
options: {...}
}

Pre-populated parts pass through validation, defaults are applied, then each part is added to the calculator’s parts list (existing parts are cleared first). Per-part validation issues are surfaced as inline errors in the UI rather than blocking the whole init.

Part Properties:

PropertyTypeDescription
lnumberPart length in mm (defaults to 1 if missing)
wnumberPart width in mm (defaults to 1 if missing)
tnumber | stringThickness (optional, inherits from stock if not set)
qnumberQuantity (default: 1)
namestringPart name/label. Normalised to UPPERCASE on intake.
materialstringMaterial identifier for stock matching. Normalised to UPPERCASE on intake — match this against the uppercase form of your stock’s material field.
orientationLock'l' | 'w' | ''Orientation constraint: length-locked, width-locked, or free
bandingobjectEdge banding — V3 nested form: { sides: { l1, l2, w1, w2 } }. Legacy V2 flat form ({ x1, x2, y1, y2 } and 0/1 boolean shorthands) is accepted and migrated automatically.
finishobjectSurface finish — V3 nested form: { faces: { a, b } }. Legacy flat { a, b } (with 0/1) is accepted and migrated.
planingobjectPlaning configuration
customDataobjectArbitrary key-value data attached to the part. This is the only field that round-trips — it appears verbatim on the matching part in the smartcut/result event, so it’s the supported way to attach your own identifiers (order refs, line-item IDs, etc.) for reconciliation.
stockIdstringLock part to a specific stock item (by stock code)
stockobjectStock reference: { db_id?, code?, material?, thickness? } — locks the part to the stock it originated from. Useful when pre-populating from an existing cut list.

Pre-populating after init. As an alternative to passing parts on init, host code can fire a smartcut/load window event whose detail matches the calculate-data structure (see smartcut/load event). The calculator will clear existing parts and import the supplied ones the same way as init-time pre-population.

Saw configuration should be provided in the saw property (recommended) or in options for backward compatibility.

saw: {
stockType: 'sheet', // 'sheet', 'linear', or 'roll'
bladeWidth: 3.2, // Blade width in mm
cutType: 'guillotine', // 'efficiency', 'guillotine', 'beam'
cutPreference: 'l', // 'l' (length), 'w' (width)
stackHeight: 60, // Maximum stack height in mm (beam saws only)
efficiencyOptions: { // Optional efficiency settings
primaryCompression: 'l' // 'l' (length) or 'w' (width) - primary compression direction
},
guillotineOptions: { // Optional guillotine settings
strategy: 'efficiency', // 'efficiency' or 'time'
maxPhase: 0, // Maximum cutting phase depth (0 = unlimited, 1-10)
headCuts: false // Enable head cuts for optimization
},
options: { // Additional saw options
stockSelection: 'efficiency', // 'efficiency' or 'smallest' - stock selection method
stackingMode: 'identical', // 'dimensions', 'identical', or 'none' - part stacking mode
minSpacing: 0 // Minimum spacing between parts in mm
}
}

Cut Type Values:

  • 'efficiency' - Standard efficiency optimization
  • 'guillotine' - Guillotine cutting (only straight cuts across entire stock)
  • 'beam' - Beam saw optimization

Cut Preference Values:

  • 'l' - Prefer length-first cuts
  • 'w' - Prefer width-first cuts

Stock Selection Values:

  • 'efficiency' - Select stock that maximizes material efficiency (default)
  • 'smallest' - Prefer smaller stock sizes when possible

Stacking Mode Values:

  • 'identical' - Stack only parts with identical dimensions and properties (default)
  • 'dimensions' - Stack parts with same dimensions regardless of other properties
  • 'none' - Disable part stacking
options: {
apiVersion: 3 // 2 or 3 (default: 3)
}

API Version Differences:

  • Version 2: Uses flat extras structure ({ banding: { x1: string, x2: string, y1: string, y2: string } })
  • Version 3: Uses nested extras structure ({ extras: { banding: { sides: { l1: string, l2: string } } } })

See Result Format for details on the differences between versions.

options: {
enable: {
emitAPIResult: true // Include full API v3 response in result (default: false)
}
}

When enabled, the result object will include an apiResultV3 property containing the complete API v3 response format. This is useful when you need the standardized API response structure regardless of which apiVersion is configured for the checkout result.

See API Result V3 in the Result Format section for the structure.

options: {
enable: {
debug: true // Enable debug output in console (default: false)
}
}

When enabled, additional debug information will be logged to the browser console during calculations.

options: {
unitSystem: 'metric' // 'metric' or 'imperial' (default: 'metric')
}

Controls the unit system used for display and input. 'imperial' switches dimensions to inches in the UI. Internally all values remain in millimetres.

options: {
stockGrain: 'l' // 'l', 'w', or '' — default grain direction applied to stock items without an explicit grain field
}
options: {
numberFormat: 'decimal', // 'decimal' or 'fraction'
decimalPlaces: 2,
fractionRoundTo: 0 // For fraction formatting
}
options: {
orientationModel: 0, // 0, 1, or 2 (controls how INPUT parts handle orientation)
resultOrientationModel: 0 // 0, 1, or 2 (controls how RESULT parts are reported)
}

Orientation Model Values:

  • 0 - No orientation control
  • 1 - Grain lock (parts maintain grain direction)
  • 2 - Dimension lock (length always maps to longer dimension)

resultOrientationModel lets you display results in a different orientation convention than the inputs (e.g. accept any orientation on input but always report results with length on the longer side).

options: {
maxParts: 100, // Maximum parts per order (0 = unlimited, default: 0)
minDimension: 10, // Minimum allowed part dimension in mm (default: 0)
partTrim: 0, // Amount to trim from each part dimension before optimisation, in mm
minSpacing: 0 // Minimum spacing between adjacent parts in mm
}

partTrim is applied symmetrically to l and w at calculation time — useful when you want to optimise on rough sizes but cut to a finished size. minSpacing adds a gap between parts independent of blade kerf.

options: {
partsPerPage: 10, // Parts shown per page when pagination enabled
enable: {
pagination: true // Enable pagination for parts list
}
}
options: {
fieldOrder: 'l, w, t, q, name, orientation' // Custom field display order
}

Control the order of input fields in the parts form. Provide a comma-separated list of field IDs.

options: {
customFields: [
{
type: 'string',
label: 'Project Name',
id: 'projectName',
placeholder: 'Enter project name'
},
{
type: 'integer',
label: 'Quantity',
id: 'quantity',
default: 1
},
{
type: 'select',
label: 'Finish',
id: 'finish',
outputType: 'string',
options: [
{ label: 'Matt', value: 'matt' },
{ label: 'Gloss', value: 'gloss' }
]
},
{
type: 'checkbox',
label: 'Rush Order',
id: 'rush',
trueValue: 'yes',
falseValue: 'no',
default: 'no'
}
]
}

Control colors, enabled features, and field order:

options: {
enable: {
// Extras + machining
banding: false, // Edge banding column on parts (default: false)
finish: false, // Finishing column on parts (default: false)
planing: false, // Planing column on parts (default: false)
machining: false, // Machining operations (holes, corners) (default: false)
// Parts list UI
orientation: true, // Part orientation lock control (default: true)
partName: true, // Allow naming parts (default: true)
pagination: false, // Paginate parts list (default: false)
progressNumber: true, // Show progress/step numbers in UI (default: true)
focus: true, // Highlight selected part in diagram on row focus (default: true)
click: true, // Allow clicking parts in the diagram to select them (default: true)
groups: false, // User-defined part groups (default: false)
fullStock: false, // "Full sheet" toggle per part — purchase entire sheet without cutting (default: false)
imageUpload: false, // Image upload column on parts (default: false)
// Diagram
diagram: true, // Show cutting diagram (default: true)
diagramNav: false, // Diagram navigation controls when multiple sheets (default: false)
// Import
csvImport: false, // CSV import for parts (default: false)
csvTemplate: false, // Show CSV template download link in import dialog (default: false)
// Diagnostics
debug: false // Verbose debug logging — also flippable via `?debug` URL param (default: false)
},
colors: {
partA: '#118ab2',
partB: '#06d6a0', // Alternating part color
stock: '#ffd166',
button: '#118ab2', // Required
buttonText: '#ffffff', // Required
headerBackground: '#ffffff', // Header background color
headerText: '#212529' // Header text color
}
}

When enable.fullStock is on, parts can be toggled to “full sheet” mode. If multiple sheet sizes exist for the chosen material, a dimension picker dialog opens; if fullSizeOnly: true is set on the stock, the part is locked into full-sheet mode automatically.

Extras are additional services like edge banding, finishing, planing, and machining that can be applied to parts with custom pricing.

const initData = {
stock: [...],
// Edge banding configuration
banding: {
labels: ['type', 'thickness'],
pricing: {
'oak|1mm': 1.0,
'oak|2mm': 1.1,
'pine|1mm': 2.0,
'maple|1mm': 3.0
}
},
// Finish configuration
finish: {
labels: ['type', 'style'],
pricing: {
'spray|matt': 1.0,
'spray|satin': 1.1,
'lacquer|matt': 2.0,
'lacquer|satin': 2.2
}
},
// Planing configuration
planing: {
labels: ['type'],
pricing: {
'standard': 0.5,
'premium': 0.8
}
},
options: {...}
}

Labels must match the pricing levels. Each pricing key is a |-joined tuple with one choice per entry in labels. The number of labels must equal the number of |-separated levels in every key — e.g. labels: ['type', 'thickness'] (2) pairs with 'oak|1mm' (2 levels). A key with the wrong level count (or a labels length that doesn’t match) means the extra can’t be built, and the widget shows “<Type> options couldn’t be loaded”.

An extra only renders when its type is enabled in options.enable (e.g. options.enable.banding = true). A disabled type ships no UI even if labels/pricing are present — and produces no error.

Catalogue-linked extras (admin-configured)

Section titled “Catalogue-linked extras (admin-configured)”

Banding and finish can instead be linked to a catalogue in the SmartCut admin (the Extras tab) rather than defined inline here. When a shop uses a catalogue-linked extra, SmartCut supplies its options to the widget automatically and the customer picks from an on-demand cascade (for example decor → finish → colour) instead of the inline labels/pricing grid above.

For these you don’t author labels/pricing — the catalogue owns both the option schema and the pricing, and large lists (such as an edge-banding decor range) are loaded on demand rather than embedded in the config. CSV-imported banding lists are handled this way. This is transparent to the embedding page: the widget renders the appropriate picker per extra. The inline labels/pricing form documented above is for shops that define their extras directly in init().

Control which sides and faces are available for each extra type by adding a locations property directly to each extra configuration:

// Available sides: 'side.l1', 'side.l2', 'side.w1', 'side.w2' (main edges)
// 'side.a', 'side.b', 'side.c', 'side.d' (corners)
// Available faces: 'face.a', 'face.b'
banding: {
labels: ['material', 'thickness'],
pricing: {...},
locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'] // Only main edges
},
finish: {
labels: ['type'],
pricing: {...},
locations: ['face.a', 'face.b'] // Both faces
},
planing: {
labels: ['type'],
pricing: {...},
locations: ['side.w1', 'side.w2', 'face.a', 'face.b'] // Edges and faces
}

Create custom groupings of locations for bulk operations with optional pricing by adding a groups array directly to each extra configuration:

planing: {
labels: ['type'],
pricing: {
'standard': 0.5,
'premium': 0.8
},
locations: ['side.w1', 'side.w2', 'face.a', 'face.b'],
groups: [
{
id: 'two-sided',
label: '2 sided',
locations: ['face.a', 'face.b'],
price: 100,
hideIndividualLocations: true // Optional: hide individual location controls when group is available
},
{
id: 'four-sided',
label: '4 sided',
locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
price: 200,
hideIndividualLocations: true,
// Optional: Group-level validation rules
rules: {
longSide: { min: 200 },
shortSide: { min: 150 },
message: '4-sided planing requires minimum 200x150mm parts'
}
}
]
},
banding: {
labels: ['material'],
pricing: {...},
groups: [
{
id: 'all-corners',
label: 'All Corners',
locations: ['side.a', 'side.b', 'side.c', 'side.d']
},
{
id: 'long-sides',
label: 'Long Sides',
locations: ['side.l1', 'side.l2']
}
]
}

Group Properties:

  • id (required) - Unique identifier for the group
  • label (required) - Display name shown in the UI
  • locations (required) - Array of location strings included in this group
  • price (optional) - Fixed price for the entire group (overrides individual location pricing)
  • hideIndividualLocations (optional) - When true, hides the individual location controls for locations in this group
  • rules (optional) - Validation rules that apply when using this group (see Group-Level Validation Rules)

Apply dimension constraints to control which parts can have extras applied by adding a rules property directly to each extra configuration:

planing: {
labels: ['type'],
pricing: {...},
locations: [...],
rules: {
t: {
min: 8,
max: 230
},
shortSide: {
min: 10,
max: 620
},
longSide: {
max: 1000
},
message: 'Planing requires thickness 8-230mm, short side 10-620mm, and long side max 1000mm'
}
},
banding: {
labels: ['material'],
pricing: {...},
rules: {
t: {
min: 12,
max: 30
},
message: 'Banding is only available for 12-30mm thick materials'
}
},
finish: {
labels: ['type'],
pricing: {...},
rules: {
longSide: {
max: 2400
},
shortSide: {
max: 1200
},
message: 'Finish is only available for parts smaller than 2400x1200mm'
}
}

Validation Rule Fields:

  • longSide.min / longSide.max - Constraints on the longer dimension
  • shortSide.min / shortSide.max - Constraints on the shorter dimension
  • t.min / t.max - Thickness constraints
  • formula (optional) - JavaScript expression for complex validation (can reference longSide, shortSide, t)
  • message - Custom error message shown when validation fails
  • locations (optional) - Array of location-specific rules (see Location-Specific Validation Rules)

When a part doesn’t meet the validation rules, the extras options will be disabled and an error message displayed.

Apply different validation constraints to individual locations within an extra type. This allows fine-grained control over which parts can use specific edges or faces:

banding: {
labels: ['material'],
pricing: {...},
rules: {
// Type-level rules (apply to all locations by default)
longSide: { min: 100 },
shortSide: { min: 50 },
message: 'Banding requires minimum 100x50mm parts',
// Location-specific rules (override type-level for specific locations)
locations: [
{
location: 'side.l1',
longSide: { min: 500 },
message: 'L1 banding only available for parts >= 500mm long side'
},
{
location: 'side.l2',
longSide: { min: 500 },
message: 'L2 banding only available for parts >= 500mm long side'
}
]
}
}

Location Rule Properties:

  • location (required) - The location this rule applies to (e.g., 'side.l1', 'face.a')
  • longSide / shortSide / t - Dimension constraints for this location
  • formula (optional) - JavaScript expression for complex validation
  • message (optional) - Custom error message for this location

Groups can have their own validation rules that apply when any location in the group is selected:

planing: {
labels: ['type'],
pricing: {...},
groups: [
{
id: 'four-sided',
label: '4 Sided',
locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
price: 200,
rules: {
longSide: { min: 200 },
shortSide: { min: 150 },
t: { min: 18 },
message: '4-sided planing requires minimum 200x150x18mm parts'
}
}
]
}

When multiple rules could apply to a location, the most specific rule wins:

  1. Location-specific rule - Rules defined in rules.locations[] for a specific location
  2. Group rule - Rules defined on a group that contains the location
  3. Type-level rule - The base rules for the extra type

Only one rule level applies per location - the most specific one completely overrides less specific rules.

Example:

banding: {
rules: {
longSide: { min: 100 }, // Type-level: default for all locations
locations: [
{ location: 'side.l1', longSide: { min: 500 } } // Location-specific: overrides for l1
]
},
groups: [
{
id: 'all-sides',
locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'],
rules: { longSide: { min: 200 } } // Group rule: applies to group members without location rules
}
]
}

In this example:

  • side.l1: Uses location rule (min 500mm) - most specific
  • side.l2, side.w1, side.w2: Use group rule (min 200mm) - group is more specific than type
  • Any other location: Uses type-level rule (min 100mm) - fallback

Apply dimension constraints to parts themselves to control which parts can be processed by adding a partRules property to the init data:

const initData = {
stock: [...],
// Part validation rules
partRules: {
// Basic dimension constraints
l: {
min: 50,
max: 2400
},
w: {
min: 20,
max: 1200
},
t: {
min: 12,
max: 30
},
longSide: {
min: 100,
max: 2400
},
shortSide: {
min: 50,
max: 1200
},
// Cross-dimensional rule: at least one side must meet primaryMin,
// and the other side must meet secondaryMin
crossDimensionalRule: {
primaryMin: 50,
secondaryMin: 20
},
// Optional custom message
message: 'At least one side must be ≥ 50 mm and the other side must be ≥ 20 mm'
},
options: {...}
}

Part Validation Rule Fields:

  • l.min / l.max - Length constraints
  • w.min / w.max - Width constraints
  • t.min / t.max - Thickness constraints
  • longSide.min / longSide.max - Constraints on the longer dimension
  • shortSide.min / shortSide.max - Constraints on the shorter dimension
  • crossDimensionalRule (optional) - Cross-dimensional validation
    • primaryMin - At least one side must be ≥ this value
    • secondaryMin - The other side must be ≥ this value
  • formula (optional) - Formula expression for complex validation (can reference l, w, t, longSide, shortSide)
    • Supports arithmetic operators: +, -, *, /
    • Supports comparison operators: >, <, >=, <=, ==
    • Supports logical operators: && (AND), || (OR)
    • Supports ternary operator: condition ? trueValue : falseValue
  • message - Custom error message shown when validation fails

Example with formula:

partRules: {
formula: '(l >= 50 && w >= 20) || (w >= 50 && l >= 20)',
message: 'At least one side must be ≥ 50 mm and the other side must be ≥ 20 mm'
}

Complex formula example:

partRules: {
formula: '(l * w) > 100000 && t >= 12',
message: 'Part must have area > 100,000 mm² and thickness ≥ 12 mm'
}

When a part doesn’t meet the validation rules, an error message will be displayed below the part and calculation will be blocked until the issue is resolved.

For more complex validation scenarios, you can use custom validation rules that support full access to part properties including extras and machining. This is configured via the EcommerceSettings admin page or by adding a customValidation property to the init data:

const initData = {
stock: [...],
// Custom validation rules
customValidation: {
enabled: true,
rules: [
{
id: 'min-size',
enabled: true,
name: 'Minimum Part Size',
formula: 'longSide >= 100 && shortSide >= 50',
message: 'Parts must be at least 100mm x 50mm'
},
{
id: 'banding-thickness',
enabled: true,
name: 'Banding Thickness Check',
formula: '!hasBanding || t >= 12',
message: 'Banding requires minimum 12mm thickness'
},
{
id: 'machining-thickness',
enabled: true,
name: 'Machining Thickness Check',
formula: '!hasMachining || t >= 18',
message: 'Machining operations require minimum 18mm thickness'
}
]
},
options: {...}
}

Custom Validation Rule Fields:

  • id - Unique identifier for the rule
  • enabled - Whether the rule is active (default: true)
  • name - User-friendly name for the rule (optional)
  • formula - Condition that must evaluate to true for the part to be valid
  • message - Error message shown when validation fails

Available Variables:

VariableTypeDescription
lnumberPart length
wnumberPart width
tnumberPart thickness
qnumberPart quantity
longSidenumbermax(l, w)
shortSidenumbermin(l, w)
materialstringMaterial name
namestringPart name
grainstringGrain direction
fullStockbooleanFull stock purchase flag
hasBandingbooleanTrue if any banding applied
hasFinishbooleanTrue if any finish applied
hasPlaningbooleanTrue if any planing applied
hasMachiningbooleanTrue if any machining operations
extras.banding.sides.l1stringBanding on long side 1
extras.banding.sides.l2stringBanding on long side 2
extras.banding.sides.w1stringBanding on short side 1
extras.banding.sides.w2stringBanding on short side 2
extras.finish.faces.astringFinish on face A
extras.finish.faces.bstringFinish on face B
machining.holesnumberNumber of holes
machining.cornersnumberNumber of corner operations

Supported Operators:

  • Arithmetic: +, -, *, /
  • Comparison: >, <, >=, <=, ==
  • Logical: && (AND), || (OR), ! (NOT)
  • Ternary: condition ? trueValue : falseValue
  • Parentheses: (expression)

Example Rules:

// Minimum area requirement
{
formula: '(l * w) >= 10000',
message: 'Parts must have minimum 10,000mm² area'
}
// Combined extras check
{
formula: '!(hasBanding && hasFinish) || longSide >= 200',
message: 'Parts with both banding and finish must be at least 200mm'
}
// Specific banding requirement
{
formula: "extras.banding.sides.l1 == '' || t >= 15",
message: 'Banding on long side 1 requires minimum 15mm thickness'
}
// Machining hole limit
{
formula: 'machining.holes <= 10',
message: 'Maximum 10 holes per part'
}

When a part fails custom validation, an error message will be displayed and calculation will be blocked until the issue is resolved.

const initData = {
stock: [
{
l: 2440,
w: 1220,
t: 18,
cost: 65,
material: 'Plywood'
}
],
// Edge banding
banding: {
labels: ['material', 'thickness'],
pricing: {
'oak|1mm': 1.0,
'oak|2mm': 1.2,
'pine|1mm': 0.8,
'maple|1mm': 1.5
},
locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'],
rules: {
t: { min: 12, max: 30 },
message: 'Banding only available for 12-30mm thick materials'
}
},
// Finishing
finish: {
labels: ['type', 'style'],
pricing: {
'spray|matt': 5.0,
'spray|satin': 5.5,
'lacquer|matt': 8.0,
'lacquer|satin': 8.5
},
locations: ['face.a', 'face.b'],
rules: {
longSide: { max: 2400 },
shortSide: { max: 1200 },
message: 'Finish is only available for parts smaller than 2400x1200mm'
}
},
// Planing
planing: {
labels: ['type'],
pricing: {
'standard': 2.0,
'premium': 3.5
},
locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
groups: [
{
id: 'two-sided',
label: 'Two Sides (Top & Bottom)',
locations: ['face.a', 'face.b'],
price: 100,
hideIndividualLocations: true
},
{
id: 'four-sided',
label: 'Four Sides (All Faces & Edges)',
locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
price: 200,
hideIndividualLocations: true
}
],
rules: {
t: { min: 8, max: 230 },
shortSide: { min: 10, max: 620 },
longSide: { max: 1000 },
message: 'Planing requires: thickness 8-230mm, width 10-620mm, length max 1000mm'
}
},
saw: {
stockType: 'sheet',
bladeWidth: 3.2,
cutType: 'guillotine',
cutPreference: 'l'
},
options: {
locale: 'en-US',
currency: 'USD',
apiVersion: 3, // 2 or 3 (default: 3)
// Enable extras features
enable: {
banding: true,
finish: true,
planing: true,
machining: false
}
}
}

Surface a product browser on the checkout so customers can pick from your catalogue before configuring parts. Independent of Product Filtering (which filters the stock list).

const initData = {
stock: [...],
products: {
enabled: true, // Show the product browser tab
showCategories: true // Group products by category in the browser
},
options: {...}
}

A user picking a “simple” product fires smartcut/product-selected; picking a “formula” (configurable) product hands off to the Configurator. Stock filter “Create Cut List” confirmations fire smartcut/selection-confirmed.

You can deep-link directly to a product by appending ?product=<productId> to the page URL; the calculator picks up the parameter on mount.

Wire up the storefront’s order-lookup view and the live-inventory watcher by supplying the host’s API config:

const initData = {
api: {
baseUrl: 'https://your-storefront.example.com',
orgSlug: 'your-org-slug',
isCustomDomain: false, // true if storefront is on a custom domain
wsServer: 'wss://your-storefront.example.com', // optional
orgId: '66abc123...' // optional Mongo org id (required with wsServer)
},
options: {...}
}

Appending ?view=track-order (or ?view=order-lookup) to the page URL opens the order-lookup view on mount instead of the calculator.

When both wsServer and orgId are set, the calculator connects via Socket.IO and fires smartcut/inventoryUpdated whenever stock changes for the org.

Enable formula-driven (parametric) products — typically used for configurable cabinets, where customer inputs (height/width/etc.) drive a JS formula that emits the parts list:

const initData = {
config: {
configurator: {
enabled: true,
url: 'https://your-host.example.com/configurator.html', // optional iframe URL
spec: { /* configurator spec, opaque to the widget */ }
}
},
options: {...}
}

A product whose type === 'formula' (via the Product Catalog) routes to the Configurator; on confirmation the calculator receives the generated parts and falls through to the normal calculate flow.

Different materials can route to different saws. When a customer picks a material whose db_sawId differs from the page-load saw, supply a per-saw map so the storefront UI can swap stock-type-driven fields, blade width, and validation without a server round-trip:

const initData = {
saw: {
/* page-load saw config */
stockType: 'sheet',
bladeWidth: 3.2,
cutType: 'efficiency'
},
sawsById: {
'66abc1...': { stockType: 'linear', bladeWidth: 1.6, cutType: 'efficiency' },
'66def2...': { stockType: 'sheet', bladeWidth: 4.0, cutType: 'guillotine', cutPreference: 'l' }
},
options: {...}
}

Source of truth for saw resolution is server-side (resolveSawFromInputStock); sawsById exists purely to keep the storefront UI consistent with what the server will actually apply at calc time. Each map value mirrors the same shape as the top-level saw field.

Advanced machining features for holes, corners, and custom operations:

const initData = {
machining: {
faces: {
enabled: true
},
holes: {
enabled: true,
defaultDiameter: 5,
minDiameter: 5,
maxDiameter: 10,
enableDepth: true,
minDepth: 5
},
hingeHoles: {
enabled: true,
minimumHoleDistance: 10,
defaultDistanceFromEdge: 22,
defaultOuterSpacing: 10,
defaultHingeLength: 50
},
shelfHoles: {
enabled: true,
diameters: [5, 10]
},
corners: {
enabled: true,
types: ['radius', 'bevel'],
enableBanding: true
}
},
options: {
enable: {
machining: true
}
}
}

The Checkout API dispatches custom window events for integration. Events fall into two groups:

  • Output events (smartcut/...) — the widget dispatches these on window; host code listens.
  • Input events (smartcut/load) — host code dispatches these; the widget listens.

Fired when the calculator is loaded and ready to initialize.

window.addEventListener('smartcut/ready', () => {
window.smartcutCheckout.init(initData)
})

Fired when initialization is complete.

window.addEventListener('smartcut/initComplete', () => {
console.log('Calculator initialized')
})

Fired when a calculation starts.

window.addEventListener('smartcut/calculating', () => {
console.log('Calculation in progress...')
})

Fired after internal validation passes but before the calculation is sent to the server. This allows you to add custom validation logic using JavaScript and optionally prevent the calculation from proceeding.

Event properties:

  • cancelable: true - Call event.preventDefault() to cancel the calculation
  • event.detail.data - The calculation data that would be sent (parts, stock, saw config, etc.)
  • event.detail.error - Set this to a string to display a custom error message
window.addEventListener('smartcut/beforeCalculate', (event) => {
const calculationData = event.detail.data
// Example: Custom validation - check total quantity
const totalQuantity = calculationData.inputShapes.reduce(
(sum, part) => sum + (part.q || 1),
0
)
if (totalQuantity > 500) {
// Prevent the calculation
event.preventDefault()
// Set a custom error message to display
event.detail.error = 'Maximum 500 parts per order. Please split your order.'
return
}
// Example: Check part dimensions
for (const part of calculationData.inputShapes) {
if (part.l > 3000 || part.w > 2000) {
event.preventDefault()
event.detail.error = `Part "${part.name || 'Unnamed'}" exceeds maximum dimensions (3000x2000mm)`
return
}
}
// Validation passed - calculation will proceed
console.log('Custom validation passed, calculating...')
})

When the calculation is prevented:

  • The calculate button is re-enabled (thinking state is reset)
  • If event.detail.error is set, the error message is displayed to the user
  • The smartcut/result event will not fire

Calculation data structure (event.detail.data):

{
inputSaw: {
stockType: string,
bladeWidth: number,
cutType: string,
cutPreference: string,
// ... other saw properties
},
inputShapes: Array<{
l: number,
w: number,
t: number | null,
q: number,
material: string | null,
name: string | null,
orientationLock: string | null,
extras?: {...},
customData?: Record<string, any>
}>,
inputStock: Array<{
l: number,
w: number,
t: number | null,
q: number,
material: string | null,
cost: number | null,
// ... other stock properties
}>,
extrasOptions: {...} | null
}

Fired when optimization results are available.

window.addEventListener('smartcut/result', (e) => {
const result = e.detail
// Optimization metadata
console.log(result.metadata)
// Parts used in optimization
console.log(result.parts)
// Stock sheets used
console.log(result.stock)
// Formatted pricing with currency
console.log(result.checkout.formattedTotalStockCost)
console.log(result.checkout.formattedBandingCost)
console.log(result.checkout.formattedFinishCost)
console.log(result.checkout.formattedPlaningCost)
})

Fired when input validation fails.

window.addEventListener('smartcut/validationError', () => {
console.log('Please check your inputs')
})

Fired when a customer picks a simple (non-configurable) product from the Product Catalog browser.

window.addEventListener('smartcut/product-selected', (event) => {
console.log('Picked product:', event.detail.product)
// event.detail = { product: Product, type: 'simple' }
})

Formula products do not emit this event — they route to the Configurator instead.

Fired when the customer presses “Create Cut List” in the Product Filtering UI (the stock filter’s confirmation step).

window.addEventListener('smartcut/selection-confirmed', () => {
console.log('Customer confirmed stock selection')
})

No detail payload — the widget scrolls to the calculator after dispatching.

Fired when the live-inventory watcher detects a stock change for the configured org. Requires api.wsServer and api.orgId to be set.

window.addEventListener('smartcut/inventoryUpdated', () => {
// Refresh your stock display or re-run a calculation
})

No detail payload.

Host-dispatched event the widget listens for. Use this to load a saved cut list after init — equivalent to pre-populating parts on init, but available at any time:

window.dispatchEvent(new CustomEvent('smartcut/load', {
detail: {
inputs: {
parts: [
{ l: 600, w: 400, t: 18, q: 4, material: 'Plywood', name: 'Shelf' },
{ l: 800, w: 300, t: 18, q: 2, material: 'Plywood', name: 'Side' }
]
}
}
}))

The widget clears existing parts and loads each part via the same path as init-time pre-population. Useful when the customer signs in and you want to restore their last basket, or when integrating with a “saved designs” feature in your storefront.

The result object returned via the smartcut/result event contains different structures based on the apiVersion setting.

When apiVersion: 2 is set, results use a flat extras structure:

{
jobId: number,
metadata: {
totalStockCost: number,
bandingLengthByType: Record<string, number>,
finishAreaByType: Record<string, number>,
planingAreaByType: Record<string, number>,
addedPartTally: Record<string, number>,
usedStockTally: Record<string, number>,
unplacedParts: Array<Part>
},
parts: Array<{
l: number,
w: number,
t: number | null,
q: number,
material: string | null,
name: string | null,
orientationLock: '' | 'l' | 'w' | null,
// Flat extras structure (API v2)
banding?: Record<string, string | boolean>, // e.g., { x1: 'oak|1mm', x2: 'oak|1mm', y1: '', y2: '' }
finish?: Record<string, string | boolean>, // e.g., { a: true, b: false }
planing?: Record<string, string | boolean>,
customData?: Record<string, any>
}>,
stock: Array<{
id: string,
name: string | null,
l: number,
w: number,
t: number | null,
material: string | null,
q: number,
trim?: { x1: number, x2: number, y1: number, y2: number },
cost?: number,
discount?: number, // Configured percentage discount (0–100), surfaced to pricing formulas
pricingFormula?: string, // Per-stock pricing override evaluated at checkout
// Analysis is aggregated across all stock with same parentId when stacking
// Most values are summed; areaEfficiency is averaged
analysis?: {
areaEfficiency: number, // Average efficiency across all stacked stock
finishArea: number, // Total finish area
bandingLength: number, // Total banding length
partArea: number, // Total part area
totalParts: number, // Total number of parts
stackedNumberOfCuts: number,
numberOfCuts: number,
stackedCutLength: number,
cutLength: number,
rollLength: number
},
sheets?: Array<{...}>, // Per-sheet analysis (same shape as `analysis` per row) — see V3 below
customData?: Record<string, any>
}>,
offcuts: Array<{
l: number,
w: number,
t: number | null,
q: number,
stockId: string
}>,
inputs: {
parts: Array<Part> // Same structure as parts above
}
}

When apiVersion: 3 is set (default), results use a nested extras structure:

{
jobId: number,
metadata: {
totalStockCost: number,
bandingLengthByType: Record<string, number>,
finishAreaByType: Record<string, number>,
planingAreaByType: Record<string, number>,
addedPartTally: Record<string, number>,
usedStockTally: Record<string, number>,
unplacedParts: Array<Part>
},
parts: Array<{
l: number,
w: number,
t: number | null,
q: number,
material: string | null,
name: string | null,
orientationLock: '' | 'l' | 'w' | null,
// Nested extras structure (API v3)
extras?: {
banding?: {
sides: Record<string, string | boolean> // e.g., { l1: 'oak|1mm', l2: 'oak|1mm', w1: '', w2: '' }
},
finish?: {
faces: Record<string, string | boolean> // e.g., { a: true, b: false }
},
planing?: {
sides?: Record<string, string | boolean>,
faces?: Record<string, string | boolean>
}
},
customData?: Record<string, any>
}>,
stock: Array<{
id: string,
name: string | null,
l: number,
w: number,
t: number | null,
material: string | null,
q: number,
trim?: { l1: number, l2: number, w1: number, w2: number }, // Note: uses l1/l2/w1/w2 in v3
cost?: number,
// Analysis is aggregated across all stock with same parentId when stacking
// Most values are summed; areaEfficiency is averaged
analysis?: {
areaEfficiency: number, // Average efficiency across all stacked stock
finishArea: number, // Total finish area
bandingLength: number, // Total banding length
partArea: number, // Total part area
totalParts: number, // Total number of parts
stackedNumberOfCuts: number,
numberOfCuts: number,
stackedCutLength: number,
cutLength: number,
rollLength: number
},
// Additional v3 fields
color?: { hex: string, name: string } | string,
weight?: number,
imageUrl?: string,
tags?: string[],
available?: boolean,
discount?: number, // Configured percentage discount (0–100), surfaced to pricing formulas
pricingFormula?: string, // Per-stock pricing override evaluated at checkout
sheets?: Array<{ // Per-sheet analysis — present when the cut server emits per-sheet data.
areaEfficiency: number, // Pricing formulas iterate these when set so usedFraction reflects each
finishArea: number, // physical sheet, not the order-wide average.
bandingLength: number,
partArea: number,
totalParts: number,
stackedNumberOfCuts: number,
numberOfCuts: number,
stackedCutLength: number,
cutLength: number,
rollLength: number
}>,
customData?: Record<string, any>
}>,
offcuts: Array<{
l: number,
w: number,
t: number | null,
q: number,
stockId: string
}>,
inputs: {
parts: Array<Part> // Same structure as parts above
}
}
window.addEventListener('smartcut/result', (e) => {
const result = e.detail
// Get total cost from metadata
const totalCost = result.metadata.totalStockCost
// Get parts with optimization data
const optimizedParts = result.parts
// Get stock usage
const stockUsed = result.stock
// Get offcuts for reuse
const offcuts = result.offcuts
// Access extras based on API version
const apiVersion = 3 // Your configured version
result.parts.forEach(part => {
if (apiVersion === 2) {
// API v2: Flat structure
const bandingX1 = part.banding?.x1
const finishFaceA = part.finish?.a
} else {
// API v3: Nested structure
const bandingL1 = part.extras?.banding?.sides?.l1
const finishFaceA = part.extras?.finish?.faces?.a
}
})
// Get extras costs breakdown from metadata
const bandingCosts = result.metadata.bandingLengthByType
const finishCosts = result.metadata.finishAreaByType
const planingCosts = result.metadata.planingAreaByType
})

When the emitAPIResult option is enabled, the result object includes an apiResultV3 property containing the complete API v3 response. This provides a standardized format matching the SmartCut API v3 specification:

{
// Standard checkout result properties...
jobId: number,
metadata: {...},
parts: [...],
stock: [...],
offcuts: [...],
inputs: {...},
// Additional API v3 response (when emitAPIResult: true)
apiResultV3?: {
jobId: number,
saw: {
stockType: string,
bladeWidth: number,
cutType: string,
cutPreference: string
},
stockList: Array<{...}>,
shapeList: Array<{...}>,
cutList: Array<{...}>,
offcuts: Array<{...}>,
unusableShapes: Array<{...}>,
metadata: {...}
}
}

This is useful when integrating with systems that expect the full API v3 response format, or when you need access to additional data like cutList that isn’t included in the standard checkout result.

window.addEventListener('smartcut/result', (e) => {
const result = e.detail
// Access the API v3 response if available
if (result.apiResultV3) {
console.log('Cut list:', result.apiResultV3.cutList)
console.log('Shape list:', result.apiResultV3.shapeList)
}
})

Enable dynamic stock selection with filtering and search:

const initData = {
stock: [
{
l: 2800,
w: 2070,
t: 18,
cost: 95,
material: 'White MDF',
code: 'MDF-2800-18',
category: 'Sheet Materials',
tags: ['mdf', 'white', 'standard'],
available: true
}
],
stockFilter: {
enabled: true,
config: {
displayMode: 'grid', // 'grid' or 'list'
enableSearch: true,
itemsPerPage: 20,
allowMultipleSelection: true,
availableFilters: [
{
field: 'material',
type: 'multiselect',
label: 'Material'
},
{
field: 't',
type: 'range',
label: 'Thickness (mm)',
min: 0,
max: 30
},
{
field: 'category',
type: 'multiselect',
label: 'Category'
}
]
}
},
options: {...}
}