Integrating Salesforce with the CubeMaster API

A step-by-step guide to calculating load plans using the https://api.cubemaster.net/loads endpoint.

To use the CubeMaster API, you need an API key (TokenID) for authentication. Here's how to get started:

  1. Visit the CubeMaster website: https://cubemaster.net.
  2. Locate the "Sign In" option (typically found in the top-right corner).
  3. Fill out the registration form with your details (e.g., name, email, password, company information).
  4. After signing up, log in to your account dashboard.
  5. Navigate to the "Settings" - "Integration" section to generate your API key (TokenID).
  6. Generate an API key. Once generated, you’ll receive a unique TokenID (e.g., abc123xyz789). Copy this key and store it securely, as it will be used in the HTTP headers of your API requests.
  7. Copy the TokenID and store it securely.

Note: The TokenID will be used in the HTTP headers of your POST request for authentication.

Before diving into Salesforce, let's clarify the concepts we'll be using:

  • REST API: A set of rules for how applications talk to each other. We make requests to specific URLs (endpoints) to get or send data.
  • Endpoint: The specific URL we will communicate with. In our case, it's https://api.cubemaster.net/loads.
  • HTTP Method: The type of request. To create a new load plan, we will use the POST method.
  • Headers: Extra information sent with our request, including our API key for authentication (TokenID) and the data format (Content-Type: application/json).
  • Request Body: The data we send to the endpoint, structured in JSON format.
  • Response Body: The data the API sends back after processing our request.
  • JSON (JavaScript Object Notation): A lightweight, human-readable format for structuring data as key-value pairs, which is the standard for most modern APIs.

We will use Named Credentials to securely store the CubeMaster API endpoint and authentication details. This is a Salesforce best practice as it separates credentials from code.

  1. In Salesforce Setup, search for and navigate to "Named Credentials".
  2. Click "New Named Credential".
  3. Fill in the details:
    • Label: CubeMaster API
    • Name: CubeMaster_API
    • URL: https://api.cubemaster.net
    • Identity Type: Named Principal
    • Authentication Protocol: Custom
    • Generate Authorization Header: Uncheck this box.
  4. Click Save.
  5. On the detail page, scroll to "Custom Headers" and click "New".
    • Header Field Name: TokenID
    • Value: Paste your CubeMaster API Key here.
  6. Click "New" again to add another header.
    • Header Field Name: Content-Type
    • Value: application/json
  7. Click Save.

To easily build the request and parse the response, we create Apex classes that mirror the JSON structure.

1. Request Wrapper Class (CubeMasterRequestWrapper.cls)
// CubeMasterRequestWrapper.cls
public class CubeMasterRequestWrapper {
    public String Title;
    public String Description;
    public List Cargoes;
    public List Containers;
    public Rules Rules;

    public class Cargo {
        public String Name;
        public Decimal Length;
        public Decimal Width;
        public Decimal Height;
        public Decimal Weight;
        public String OrientationsAllowed;
        public Boolean TurnAllowedOnFloor;
        public Integer Qty;
        public String ColorKnownName;
    }

    public class Container {
        public String VehicleType;
        public String Name;
        public Decimal Length;
        public Decimal Width;
        public Decimal Height;
        public String ColorKnownName;
    }

    public class Rules {
        public Boolean IsWeightLimited;
        public Boolean IsSequenceUsed;
        public String FillDirection;
        public String CalculationType;
    }
}
2. Response Wrapper Class (CubeMasterResponseWrapper.cls)
// CubeMasterResponseWrapper.cls
public class CubeMasterResponseWrapper {
    public String status;
    public String message;
    public String calculationError;
    public Document document;
    public LoadSummary loadSummary;
    public List filledContainers;

    public class Document {
        public String title;
        public String createdBy;
        public Datetime createdAt;
    }

    public class LoadSummary {
        public Integer cargoesLoaded;
        public Integer piecesLeft;
        public Decimal volumeLoaded;
        public Decimal weightLoaded;
    }

    public class FilledContainer {
        public String name;
        public ContainerLoadSummary loadSummary;
        public Graphics graphics;
    }

    public class ContainerLoadSummary {
        public Decimal volumeUtilization;
        public Decimal floorUtilization;
        public Decimal weightLoaded;
    }
    
    public class Graphics {
        public Images images;
    }

    public class Images {
        public String path3DDiagram;
        public String pathComposite;
    }
}

This class contains the logic to build the request, make the callout, and process the response. This example assumes you have custom objects Shipment__c and a related child Shipment_Item__c.

Apex Service Class (CubeMasterService.cls)
// CubeMasterService.cls
public class CubeMasterService {

    // Use @future for callouts from Triggers or other synchronous processes
    @future(callout=true)
    public static void calculateLoadPlan(Id shipmentId) {
        
        // 1. Query Salesforce data
        Shipment__c shipment = [SELECT Id, Name, Description__c, 
                                (SELECT Name, Product_Name__c, Quantity__c, Length__c, Width__c, Height__c, Weight__c
                                 FROM Shipment_Items__r) 
                                FROM Shipment__c WHERE Id = :shipmentId];

        // 2. Build the Request Body using our wrapper class
        CubeMasterRequestWrapper requestBody = new CubeMasterRequestWrapper();
        requestBody.Title = shipment.Name;
        requestBody.Description = shipment.Description__c;
        
        // --- Cargoes ---
        requestBody.Cargoes = new List();
        for (Shipment_Item__c item : shipment.Shipment_Items__r) {
            CubeMasterRequestWrapper.Cargo c = new CubeMasterRequestWrapper.Cargo();
            c.Name = item.Product_Name__c;
            c.Qty = Integer.valueOf(item.Quantity__c);
            c.Length = item.Length__c;
            c.Width = item.Width__c;
            c.Height = item.Height__c;
            c.Weight = item.Weight__c;
            c.OrientationsAllowed = 'OrientationsAll';
            c.TurnAllowedOnFloor = false;
            c.ColorKnownName = 'Brown';
            requestBody.Cargoes.add(c);
        }

        // --- Containers ---
        requestBody.Containers = new List();
        CubeMasterRequestWrapper.Container container = new CubeMasterRequestWrapper.Container();
        container.VehicleType = 'Dry';
        container.Name = '53FT-Intermodal';
        container.Length = 630;
        container.Width = 98;
        container.Height = 106;
        container.ColorKnownName = 'Blue';
        requestBody.Containers.add(container);

        // --- Rules ---
        requestBody.Rules = new CubeMasterRequestWrapper.Rules();
        requestBody.Rules.IsWeightLimited = true;
        requestBody.Rules.IsSequenceUsed = false;
        requestBody.Rules.FillDirection = 'FrontToRear';
        requestBody.Rules.CalculationType = 'MixLoad';
        
        String jsonPayload = JSON.serialize(requestBody, true);
        System.debug('Request Payload: ' + jsonPayload);

        // 3. Perform the HTTP Callout
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('callout:CubeMaster_API/v1/loads');
        request.setMethod('POST');
        request.setBody(jsonPayload);

        try {
            HttpResponse response = http.send(request);

            // 4. Process the Response
            if (response.getStatusCode() == 200 || response.getStatusCode() == 201) {
                CubeMasterResponseWrapper responseWrapper = (CubeMasterResponseWrapper) JSON.deserialize(response.getBody(), CubeMasterResponseWrapper.class);

                // 5. Update Salesforce record with results
                if (responseWrapper.status == 'succeed' && !responseWrapper.filledContainers.isEmpty()) {
                    CubeMasterResponseWrapper.FilledContainer firstContainer = responseWrapper.filledContainers[0];
                    
                    Shipment__c shipmentToUpdate = new Shipment__c(Id = shipmentId);
                    shipmentToUpdate.Volume_Utilization__c = firstContainer.loadSummary.volumeUtilization;
                    shipmentToUpdate.Weight_Loaded__c = firstContainer.loadSummary.weightLoaded;
                    shipmentToUpdate.Load_Plan_Status__c = 'Success';
                    shipmentToUpdate.Load_Plan_3D_Image_URL__c = firstContainer.graphics.images.path3DDiagram;

                    update shipmentToUpdate;
                } else {
                    handleError(shipmentId, 'CubeMaster Error: ' + responseWrapper.message);
                }
            } else {
                handleError(shipmentId, 'HTTP Error: ' + response.getStatusCode() + ' ' + response.getStatus());
            }
        } catch (Exception e) {
            handleError(shipmentId, 'Apex Exception: ' + e.getMessage());
        }
    }

    private static void handleError(Id shipmentId, String errorMessage) {
        Shipment__c shipmentToUpdate = new Shipment__c(
            Id = shipmentId,
            Load_Plan_Status__c = 'Error',
            Load_Plan_Error_Message__c = errorMessage.left(255)
        );
        update shipmentToUpdate;
    }
}

You need a way to run the service class. A trigger that fires when a checkbox is checked is a common and effective method.

1. Add New Fields to your `Shipment__c` Object

In Object Manager, add the following fields to your `Shipment__c` custom object:

  • Calculate_Load_Plan__c (Checkbox, default unchecked)
  • Load_Plan_Status__c (Text or Picklist)
  • Volume_Utilization__c (Percent)
  • Weight_Loaded__c (Number)
  • Load_Plan_3D_Image_URL__c (URL)
  • Load_Plan_Error_Message__c (Text)
2. Create an Apex Trigger (ShipmentTrigger.trigger)
// ShipmentTrigger.trigger
trigger ShipmentTrigger on Shipment__c (after update) {
    if (Trigger.isAfter && Trigger.isUpdate) {
        for (Shipment__c newShipment : Trigger.new) {
            Shipment__c oldShipment = Trigger.oldMap.get(newShipment.Id);

            // Check if the "Calculate" checkbox was changed from false to true
            if (newShipment.Calculate_Load_Plan__c == true && oldShipment.Calculate_Load_Plan__c == false) {
                
                // Call the future method to perform the callout
                CubeMasterService.calculateLoadPlan(newShipment.Id);

                // Immediately uncheck the box to prevent re-firing.
                // This is safe because the DML for the callout is in a future context.
                newShipment.Calculate_Load_Plan__c = false; 
            }
        }
    }
}

Once everything is set up, the end-to-end process will be seamless for the user:

  1. A user creates a Shipment__c record and adds its related Shipment_Item__c records.
  2. When ready, they check the "Calculate Load Plan" box on the Shipment record and save.
  3. The ShipmentTrigger fires and calls the CubeMasterService future method.
  4. The service class builds the JSON request from the shipment data and sends it to the CubeMaster API.
  5. CubeMaster calculates the optimal load plan and sends back a JSON response.
  6. The service class parses the response and updates the original Shipment__c record with the results, including volume utilization, weight, status, and the 3D image URL.