Skip to content

Checkout JavaScript Widget

SmartCut Checkout is an embedded JavaScript widget that provides a complete cut calculator for your e-commerce or quotation website.

Overview

SmartCut Checkout allows you to:

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

Quick Start

Installation

Include the script and default css styling in your HTML:

html
<link rel="stylesheet" href="https://cutlistevo.com/checkout/checkout.css" />
<script type="module" src="https://cutlistevo.com/checkout/checkout.js"></script>

Basic Usage

html
<!-- 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>

Configuration

Stock Configuration

Define the available stock materials:

javascript
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)
  }
}

Saw Options

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

Complete Saw Configuration

javascript
saw: {
  stockType: 'sheet', // 'sheet', 'linear', or 'roll'
  bladeWidth: 3.2, // Blade width in mm
  cutType: 'guillotine', // 'efficiency', 'guillotine', 'beam', 'none'
  cutPreference: 'l', // 'l' (length), 'w' (width), 'none'
  stackHeight: 60, // Maximum stack height in mm (optional)
  guillotineOptions: { // Optional guillotine settings
    headCuts: true,
    strategy: 'efficiency', // 'efficiency' or 'time'
    maxPhase: 3
  }
}

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

Configuration Options

API Version

javascript
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.

Emit API Result

javascript
options: {
  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.

Number Format

javascript
options: {
  numberFormat: 'decimal', // 'decimal' or 'fraction'
  decimalPlaces: 2,
  fractionRoundTo: 0 // For fraction formatting
}

Orientation Model

javascript
options: {
  orientationModel: 0, // 0, 1, or 2 (input parts)
  resultOrientationModel: 0 // 0, 1, or 2 (result parts)
}

Orientation Model Values:

  • 0 - No orientation preference
  • 1 - Length-preferred orientation
  • 2 - Width-preferred orientation

Custom Fields

javascript
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'
    }
  ]
}

UI Customization

Control colors, enabled features, and field order:

javascript
options: {
  enable: {
    banding: true,
    finish: true,
    planing: true,
    machining: true,
    diagram: true,
    orientation: true,
    csvImport: false,
    partName: true,
    focus: false,
    click: true
  },
  colors: {
    partA: '#118ab2',
    stock: '#ffd166',
    button: '#118ab2',
    buttonText: '#ffffff',
    text: '#000000'
  }
}

Extras Configuration

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

Basic Extras Setup

javascript
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: {...}
}

Extras Location Filtering

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

javascript
// 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
}

Extras Location Groups

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

javascript
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, // Optional: fixed price for this group
      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
    }
  ]
},

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

Extras Validation Rules

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

javascript
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

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

Part Validation Rules

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

javascript
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:

javascript
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:

javascript
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.

Complete Extras Example

javascript
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
    }
  }
}

Machining Configuration

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

javascript
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
    }
  }
}

Events

The Checkout API dispatches custom window events for integration:

smartcut/ready

Fired when the calculator is loaded and ready to initialize.

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

smartcut/initComplete

Fired when initialization is complete.

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

smartcut/calculating

Fired when a calculation starts.

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

smartcut/result

Fired when optimization results are available.

javascript
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)
})

smartcut/validationError

Fired when input validation fails.

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

Result Format

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

API Version 2 Result Structure

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

typescript
{
  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,
    // 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
    },
    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
  }
}

API Version 3 Result Structure

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

typescript
{
  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,
    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
  }
}

Using Result Data

javascript
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
})

API Result V3

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:

typescript
{
  // 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.

javascript
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)
  }
})

Product Filtering

Enable dynamic stock selection with filtering and search:

javascript
const initData = {
  stock: [
    {
      l: 2800,
      w: 2070,
      t: 18,
      cost: 95,
      material: 'MDF',
      name: 'White MDF 2800×2070×18mm',
      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: {...}
}

Support