Step-by-Step Guide: Consuming CubeMaster API with Go (net/http)

This guide walks you through integrating the CubeMaster API's /loads endpoint using Go's net/http package. It includes authentication, legacy data integration, and response handling.

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.

A RESTful API (Representational State Transfer) is a way for applications to communicate over the internet using standard HTTP methods. Here’s a quick breakdown:

  • HTTP Methods:
    • GET: Retrieve data.
    • POST: Send data to create something (used in this guide).
    • PUT: Update data.
    • DELETE: Remove data.
  • Endpoints: URLs like https://api.cubemaster.net/loads define where to send requests.
  • JSON: A common data format for sending and receiving information (used in this API).
  • Headers: Metadata sent with requests, like authentication tokens.
  • Status Codes: Responses like 200 OK (success) or 401 Unauthorized (authentication failed).

In this guide, we’ll use a POST request to send load data to the CubeMaster API and receive a response with loading details.

Ensure you have Go installed and set up:

  1. Download and install Go from golang.org/dl/.
  2. Verify installation: go version (e.g., go1.21.0).
  3. Create a new project directory: mkdir cubemaster-api && cd cubemaster-api.
  4. Initialize a Go module: go mod init cubemaster-api.
  5. No external packages are needed since we’re using the standard net/http library.

Create a file named main.go to write your code.

Assume your customer’s legacy database stores order or shipment data (e.g., items, quantities, dimensions). Here’s how to fetch and map it to the API request:

package main

import (
    "database/sql"
    _ "github.com/go-sql-driver/mysql" // Example: MySQL driver
    "log"
)

type Item struct {
    Name      string
    Length    float64
    Width     float64
    Height    float64
    Weight    float64
    Qty       int
    Color     string
}

func fetchItemsFromDB() ([]Item, error) {
    db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/legacy_db")
    if err != nil {
        return nil, err
    }
    defer db.Close()

    rows, err := db.Query("SELECT name, length, width, height, weight, qty, color FROM shipments")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var items []Item
    for rows.Next() {
        var item Item
        if err := rows.Scan(&item.Name, &item.Length, &item.Width, &item.Height, &item.Weight, &item.Qty, &item.Color); err != nil {
            return nil, err
        }
        items = append(items, item)
    }
    return items, nil
}

This example uses a MySQL database. Adjust the driver and query based on your legacy system (e.g., PostgreSQL, SQLite). The retrieved data will be used in the API request.

Use net/http to build the POST request with the TokenID in the header and JSON payload:

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

type Cargo struct {
    Name               string  `json:"Name"`
    Length             float64 `json:"Length"`
    Width              float64 `json:"Width"`
    Height             float64 `json:"Height"`
    Weight             float64 `json:"Weight"`
    OrientationsAllowed string `json:"OrientationsAllowed"`
    TurnAllowedOnFloor bool   `json:"TurnAllowedOnFloor"`
    Qty                int    `json:"Qty"`
    ColorKnownName     string `json:"ColorKnownName"`
}

type Container struct {
    VehicleType    string `json:"VehicleType"`
    Name           string `json:"Name"`
    Length         float64 `json:"Length"`
    Width          float64 `json:"Width"`
    Height         float64 `json:"Height"`
    ColorKnownName string `json:"ColorKnownName"`
}

type Rules struct {
    IsWeightLimited bool   `json:"IsWeightLimited"`
    IsSequenceUsed  bool   `json:"IsSequenceUsed"`
    FillDirection   string `json:"FillDirection"`
    CalculationType string `json:"CalculationType"`
}

type LoadRequest struct {
    Title       string      `json:"Title"`
    Description string      `json:"Description"`
    Cargoes     []Cargo     `json:"Cargoes"`
    Containers  []Container `json:"Containers"`
    Rules       Rules       `json:"Rules"`
}

func buildRequest() (*http.Request, error) {
    // Fetch items from legacy database
    items, err := fetchItemsFromDB()
    if err != nil {
        return nil, err
    }

    // Map database items to Cargoes
    var cargoes []Cargo
    for _, item := range items {
        cargoes = append(cargoes, Cargo{
            Name:               item.Name,
            Length:             item.Length,
            Width:              item.Width,
            Height:             item.Height,
            Weight:             item.Weight,
            OrientationsAllowed: "OrientationsAll",
            TurnAllowedOnFloor: false,
            Qty:                item.Qty,
            ColorKnownName:     item.Color,
        })
    }

    // Define the request payload
    payload := LoadRequest{
        Title:       "New Mixed Truck Load",
        Description: "Hello Web API",
        Cargoes:     cargoes,
        Containers: []Container{
            {
                VehicleType:    "Dry",
                Name:           "53FT-Intermodal",
                Length:         630,
                Width:          98,
                Height:         106,
                ColorKnownName: "Blue",
            },
        },
        Rules: Rules{
            IsWeightLimited: true,
            IsSequenceUsed:  false,
            FillDirection:   "FrontToRear",
            CalculationType: "MixLoad",
        },
    }

    // Marshal to JSON
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return nil, err
    }

    // Create the POST request
    req, err := http.NewRequest("POST", "https://api.cubemaster.net/loads", bytes.NewBuffer(jsonData))
    if err != nil {
        return nil, err
    }

    // Set headers
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("TokenID", os.Getenv("CUBEMASTER_TOKENID")) // Load TokenID from environment

    return req, nil
}

Request JSON Example:

{
    "Title": "New Mixed Truck Load",
    "Description": "Hello Web API",
    "Cargoes": [
        {
            "Name": "ITEM001",
            "Length": 72,
            "Width": 30,
            "Height": 75,
            "Weight": 1002.45,
            "OrientationsAllowed": "OrientationsAll",
            "TurnAllowedOnFloor": false,
            "Qty": 16,
            "ColorKnownName": "Brown"
        },
        {
            "Name": "ITEM002",
            "Length": 27.31,
            "Width": 37.5,
            "Height": 76.67,
            "Weight": 521.45,
            "OrientationsAllowed": "OrientationsAll",
            "TurnAllowedOnFloor": false,
            "Qty": 28,
            "ColorKnownName": "Aqua"
        },
        {
            "Name": "SKU0005",
            "Length": 27.31,
            "Width": 9.5,
            "Height": 75.67,
            "Weight": 501.45,
            "OrientationsAllowed": "OrientationsAll",
            "TurnAllowedOnFloor": true,
            "Qty": 24,
            "ColorKnownName": "Beige"
        },
        {
            "Name": "SKU0005",
            "Qty": 23
        },
        {
            "Name": "SKU0008",
            "Qty": 34
        }
    ],
    "Containers": [
        {
            "VehicleType": "Dry",
            "Name": "53FT-Intermodal",
            "Length": 630,
            "Width": 98,
            "Height": 106,
            "ColorKnownName": "Blue"
        }
    ],
    "Rules": {
        "IsWeightLimited": true,
        "IsSequenceUsed": false,
        "FillDirection": "FrontToRear",
        "CalculationType": "MixLoad"
    }
}

The TokenID is added to the header for authentication, and the payload is constructed with data from the legacy database.

Use http.Client to send the request and process the response:

package main

import (
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
)

type LoadResponse struct {
    Status          string `json:"status"`
    Message         string `json:"message"`
    CalculationError string `json:"calculationError"`
    Document        struct {
        Title              string  `json:"title"`
        Description        string  `json:"description"`
        CalculationTimeInSeconds float64 `json:"calculationTimeInSeconds"`
        CreatedBy          string  `json:"createdBy"`
        CreatedAt          string  `json:"createdAt"`
    } `json:"document"`
    LoadSummary struct {
        CargoesLoaded int     `json:"cargoesLoaded"`
        PiecesLoaded  int     `json:"piecesLoaded"`
        VolumeLoaded  float64 `json:"volumeLoaded"`
        WeightLoaded  float64 `json:"weightLoaded"`
        ContainersLoaded int  `json:"containersLoaded"`
    } `json:"loadSummary"`
    FilledContainers []struct {
        Name        string `json:"name"`
        LoadSummary struct {
            VolumeUtilization float64 `json:"volumeUtilization"`
            WeightLoaded      float64 `json:"weightLoaded"`
        } `json:"loadSummary"`
        Graphics struct {
            Images struct {
                Path3DDiagram string `json:"path3DDiagram"`
            } `json:"images"`
        } `json:"graphics"`
    } `json:"filledContainers"`
}

func main() {
    // Build the request
    req, err := buildRequest()
    if err != nil {
        log.Fatalf("Error building request: %v", err)
    }

    // Create HTTP client and send request
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Fatalf("Error sending request: %v", err)
    }
    defer resp.Body.Close()

    // Check status code
    if resp.StatusCode != http.StatusOK {
        body, _ := ioutil.ReadAll(resp.Body)
        log.Fatalf("Request failed with status %d: %s", resp.StatusCode, string(body))
    }

    // Read and unmarshal response
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("Error reading response: %v", err)
    }

    var loadResp LoadResponse
    if err := json.Unmarshal(body, &loadResp); err != nil {
        log.Fatalf("Error unmarshaling response: %v", err)
    }

    // Handle the response
    log.Printf("Status: %s", loadResp.Status)
    log.Printf("Message: %s", loadResp.Message)
    if loadResp.CalculationError != "" {
        log.Printf("Calculation Error: %s", loadResp.CalculationError)
    }
    log.Printf("Load Summary: %d cargoes, %d pieces, %.2f volume, %.2f weight",
        loadResp.LoadSummary.CargoesLoaded,
        loadResp.LoadSummary.PiecesLoaded,
        loadResp.LoadSummary.VolumeLoaded,
        loadResp.LoadSummary.WeightLoaded)
    for _, container := range loadResp.FilledContainers {
        log.Printf("Container %s: %.2f%% volume utilization, %.2f weight",
            container.Name, container.LoadSummary.VolumeUtilization, container.LoadSummary.WeightLoaded)
        log.Printf("3D Diagram: %s", container.Graphics.Images.Path3DDiagram)
    }
}

Response JSON Example:

{
    "status": "succeed",
    "message": "Engine created. 5 cargoes. 1 empty containers. Calculation started. Calculation ended. The load built successfully. The load saved to the cloud database.",
    "calculationError": "InvalidCargoSize",
    "document": {
        "title": "New Mixed Truck Load",
        "description": "Hello Web API",
        "isShared": true,
        "isAutoSaved": true,
        "isPending": false,
        "calculationTimeInSeconds": 0.6152743,
        "processId": "",
        "batchId": "",
        "createdBy": "CHANG@LOGEN.CO.KR",
        "createdAt": "2023-02-11T01:17:01.7392204+09:00",
        "updatedAt": "0001-01-01T00:00:00"
    },
    "loadSummary": {
        "cargoesLoaded": 68,
        "piecesLoaded": 68,
        "cargoesLeft": 0,
        "piecesLeft": 57,
        "unitloadsLoaded": 0,
        "volumeLoaded": 5261723.4606,
        "weightLoaded": 42674.59999999999,
        "priceLoaded": 0,
        "containersLoaded": 1
    },
    "filledContainers": [
        {
            "name": "#1 53FT-Intermodal",
            "sequence": 1,
            "loadSummary": {
                "cargoesLoaded": 68,
                "piecesLoaded": 68,
                "unitloadsLoaded": 0,
                "volumeLoaded": 5261723.4606,
                "volumeUtilization": 80.39990374424703,
                "floorLoaded": 57090.75,
                "floorUtilization": 92.46963070942662,
                "weightLoaded": 42674.59999999999,
                "weightTotal": 42674.59999999999,
                "dimWeight": 39424.33734939759,
                "priceLoaded": 0,
                "cargoesPerLayer": 14,
                "layersPerUnitload": 0
            },
            "graphics": {
                "images": {
                    "path3DDiagram": "https://api.cubemaster.net/runtimes/b28413ca_51ed_44c9_b92e_13147363fd61.PNG",
                    "pathComposite": "https://api.cubemaster.net/runtimes/7eb09974_2f1d_41bc_9371_2002658dce07.PNG"
                }
            }
        }
    ]
}

The response is parsed into a struct, and key details (e.g., status, load summary, container utilization) are logged. Adjust the struct fields to capture more data as needed.

To ensure your API integration works smoothly, add monitoring and debugging:

  1. Logging: Use log.Printf to track request/response details (already included in Step 6).
  2. HTTP Debugging: Use http.Client with a custom transport to log request/response details:
  3. import (
        "net/http"
        "net/http/httputil"
    )
    
    func main() {
        req, err := buildRequest()
        if err != nil {
            log.Fatalf("Error building request: %v", err)
        }
    
        // Dump request for debugging
        dump, _ := httputil.DumpRequestOut(req, true)
        log.Printf("Request:\n%s", dump)
    
        // Custom transport for debugging
        client := &http.Client{
            Transport: &http.Transport{
                Proxy: http.ProxyFromEnvironment,
            },
        }
        resp, err := client.Do(req)
        if err != nil {
            log.Fatalf("Error sending request: %v", err)
        }
        defer resp.Body.Close()
    
        // Dump response for debugging
        dumpResp, _ := httputil.DumpResponse(resp, true)
        log.Printf("Response:\n%s", dumpResp)
    }
    
  4. Error Handling: Check resp.StatusCode and log detailed errors (e.g., 401 Unauthorized if TokenID is invalid).
  5. Monitoring: Add metrics (e.g., request latency, success rate) using a library like prometheus/client_golang if needed.
  6. Testing: Use tools like Postman to manually test the API with your TokenID and JSON payload.

Run your program with go run main.go and check logs for issues. Set the TokenID environment variable: export CUBEMASTER_TOKENID=your_token_here.