Skip to content

Using the API

Subscription Tier Required

This feature requires the Premier subscription tier or higher.

There are situations where it is useful to interact with GS without using a web browser. Some examples include:

  • Automated data collection with no user interaction.
  • Synchronizing data between GS and another system.
  • Building an alternative frontend for users to interact with.

This guide will walk through setting up the system in the same was as the Getting Started, using the API. Although this guide is available in Python and Javascript, any programming language capable of making HTTP requests may be used.

This guide assumes that the reader has a basic understanding of the chosen programming language and the ability to use a command line interface. All steps involving the command line in this guide were completed using Windows PowerShell, but can easily be translated to any other command line environment (CMD, Bash, Zsh).

Warning

If you have already followed the Getting Started guide, you will need to update the names of the entities created in this guide, or you will receive 409 errors, indicating that you are attempting to create duplicate entities.

Creating an API Key

Before the API can be used, an API key must be created.

  1. Navigate to the API Key page.
  2. Press the Add button.
  3. Fill in the Name and Role fields.
  4. Press the Save button.
  5. Copy the resulting Secret.

Information

For the purpose of this guide, it is assumed that the API Key will have at least the Engineer role. In production, it is recommended that API Keys be given the most restrictive Role possible.

Storing API Keys

The examples provided in this guide have the API key inserted directly into the source code. This is done for illustration purposes only, and should not be used in production.

Some common ways to store and load secrets are:

The exact solution used to protect your API keys will depend on the nature of the application being developed.

Principles

The GS API may be accessed by issuing an HTTP request to the appropriate API URL. The origin of the GS API for your account may be found on the Account Info page.

Authentication

Two HTTP headers are required in order to authenticate with the GS API. If these headers are not provided, the user will receive a 401 Unauthorized status.

Header Value
GS-Api-Key The value of the API key copied from Creating an API Key.
GS-Device-Id The unique ID of the device making the request. While any value will be accepted, this value should remain consistent for a given physical device.

Request Method

The GS API accepts the following request methods:

  • GET: Used to retrieve entities or data.
  • POST: Used to create or update entities and data.
  • DELETE: Used to delete entities and data.

For information about which method to use for a given endpoint, see the API Reference.

Request Body

Unless otherwise specified in the API Reference, the request body POST requests should be provided as serialized JSON, and the Content-Type header should be set to application/json.

GET and DELETE requests should never provide a body.

Responses

Unless otherwise specified in the API Reference, responses will be returned as JSON. The reference contains the specific JSON fields which will be returned for each request.

If the request was executed successfully, GS will return a 200 OK status. Other status codes indicate that the request did not complete successfully.

Status Code Description Troubleshooting
200 The request completed successfully.
400 The request was not valid. Check the API documentation to ensure that the correct fields were supplied.
Check the response body for additional information about which fields were not valid.
401 The request did not supply valid credentials. Check that all required headers were supplied.
Check that the API Key exists and is correct.
403 The supplied credentials are valid, but the API key issuing the request does not have access to the endpoint. Check that the Role of the API Key has access to the requested resource.
404 The requested entity does not exist. Check that the ID given to the endpoint exists.
Check that the correct endpoint is being used.
409 The request attempted to create an entity which already exists. Check that the name of the resource being created does not already exist. It may have been hidden.
429 Too many requests have been made. Wait longer between issuing requests.
Contact sales to discuss increasing your account rate limits.
500 The server encountered an error when attempting to process the request. The response may contain additional details about the error.
Contact support to report a bug.
501 The request attempted to access a feature which has not been implemented. Contact support to request that the feature is prioritized for development.
503 The server is temporarily unavailable. Check if routine maintenance is being performed.
Contact support if this error is received unexpectedly.

Creating the Project

This guide has the following requirements:

  • Python is installed with a version >= 3.8.
  • Familiarity with the requests Python library

Begin by setting up a new project:

Information

Although it is not necessary to complete this guide, it is recommended to utilize virtual environments for Python development.

  1. Create a new folder.
  2. Navigate to the folder.
  3. Install the requests module.
  4. Create a file named app.py.
  5. Open app.py in your favorite code editor.
  1. Create a new folder.
  2. Navigate to the folder.
  3. Create a file named app.js.
  4. Open app.js in your favorite code editor.

Next, add some helper functions to access the API:

import requests

API_KEY = "YOUR API KEY HERE"
BASE_URL = "YOUR URL"
DEVICE_ID = "MACHINE-1"

def gs_request(endpoint, method, query_params = None, body = None):
    # Establish the necessary headers
    headers = {
        "GS-Api-Key": API_KEY,
        "GS-Device-Id": DEVICE_ID
    }

    # Construct the URL
    url = f"{BASE_URL}{endpoint}"

    if method == "GET":
        # Handle GET requests, which have no body
        return requests.get(url, headers=headers, params=query_params)
    elif method == "DELETE":
        # Handle DELETE requests, which have no body
        return requests.delete(url, headers=headers, params=query_params)
    elif method == "POST":
        # Handle POST requests, and let the API know that JSON is being passed in
        headers["Content-Type"] = "application/json"
        return requests.post(url, headers=headers, json=body, params=query_params)
    else:
        raise ValueError("Invalid method type.")
const API_KEY = 'YOUR API KEY HERE';
const BASE_URL = 'YOUR URL';
const DEVICE_ID = 'MACHINE-1';

function gsRequest(endpoint, method, queryParams, body) {
    // Establish the necessary headers.
    const headers = {
        "GS-Api-Key": API_KEY,
        "GS-Device-Id": DEVICE_ID
    };
    if (method === 'POST') {
        headers['Content-Type'] = 'application/json';
    }

    // Construct the full URL
    let url = `${BASE_URL}${endpoint}`;

    // Add URL parameters
    if (queryParams) {
        url += '?' + new URLSearchParams(queryParams).toString();
    }

    return fetch(url, {
        method: method,
        headers: headers,
        body: body ? JSON.stringify(body) : undefined,
    });
}

The API can now be tested. In the same file, try retrieving your Customer's name from the Permissions endpoint:

At the bottom of the file, add:

def run():
    response = gs_request("admin/v1/user/permissions", "GET").json()
    print(response["customerName"])

if __name__ == "__main__":
    run()

At the bottom of the file, add the following:

async function run() {
    const response = await gsRequest('admin/v1/user/permissions', 'GET');
    const json = await response.json();
    console.log(json.customerName);
}

run();

Finally, execute your code:

python app.py
node app.js

If the program executed successfully, you should see the name of your GS customer.

Set Up Entities

Now that the GS API has been connected to successfully, you may begin setting up entities.

Create a Process

In order to create a Process, replace the contents of the run function with a call to the GS API using the POST method:

process_id = gs_request("setup/v1/process", "POST", None, {
    "name": "Welding",
    "sampleSize": 10,
    "ncuCost": 0.0
}).json()
const response = await gsRequest('setup/v1/process', 'POST', undefined, {
    name: "Welding",
    sampleSize: 10,
    ncuCost: 0.0
});
const processId = await response.json();

Storing the result of the call in the processId variable allows you to keep track of the ID of the newly created process so that it may be used to insert data later.

However, this assumes that a Process named Welding does not already exist. Instead of assuming that the Process must be created, first attempt to retrieve the ID, and only create it if it does not exist. Replace the above code:

# Attempt to retrieve the process using the name as a query parameter.
get_process_by_name_response = gs_request("setup/v1/process/name", "GET", { "name": "Welding" })

if get_process_by_name_response.status_code == 200:
    #The Welding process was found
    process_id = get_process_by_name_response.json()
else:
    # The process could not be found.  Create it instead.
    process_id = gs_request("setup/v1/process", "POST", None, {
        "name": "Welding",
        "sampleSize": 10,
        "ncuCost": 0.0
    }).json()
let processId;

// Attempt to retrieve the process using the name as a query parameter.
const getProcessByNameResponse = await gsRequest('setup/v1/process/name', 'GET', { name: 'Welding' });

if (getProcessByNameResponse.status === 200) {
    // The Welding process was found
    processId = await getProcessByNameResponse.json();
} else {
    // The process could not be found.  Create it instead.
    const createProcessResponse = await gsRequest('setup/v1/process', 'POST', undefined, {
        name: 'Welding',
        sampleSize: 10,
        ncuCost: 0.0
    })
    processId = await createProcessResponse.json();
}

Several entities will be created using this pattern. As an exercise, see if you can write a function to first check if an entity exists and create it if it does not.

Create Traceability

Traceability can be created in a similar method:

get_traceability_by_name_response = gs_request("setup/v1/trace/name", "GET", { "name": "Shift" })
if get_traceability_by_name_response.status_code == 200:
    traceability_id = get_traceability_by_name_response.json()
else:
    traceability_id = gs_request("setup/v1/trace", "POST", None, {
        "name": "Shift",
        "type": "text",
        "filterRTFs": False,
        "required": "never",
        "isValueSpecific": False,
        "textOptions": {
            "restricted": False
        }
    }).json()
let traceabilityId;
const getTraceabilityByNameResponse = await gsRequest('setup/v1/trace/name', 'GET', { name: 'Shift' });

if (getTraceabilityByNameResponse.status === 200) {
    traceabilityId = await getTraceabilityByNameResponse.json();
} else {
    const createTraceabilityResponse = await gsRequest('setup/v1/trace', 'POST', undefined, {
        name: 'Shift',
        type: 'text',
        filterRTFs: false,
        required: 'never',
        isValueSpecific: false,
        textOptions: {
            restricted: false
        }
    })
    traceabilityId = await createTraceabilityResponse.json();
}

Create a Part and Unit

Next, create a Part.

As an exercise, use the API to create a Part named 3-Inch Bolt. Then create a Unit named Inches. Make sure you store the ID of the part and unit for use later. Reference the API documentation to look up the Create endpoints for Parts and Units. If you get stuck, the full code for this guide is available at the bottom of the article.

Create a Characteristic

Although the code for creating a Characteristic with the API follows the same pattern as the entities created above, it requires links to the previous entities, which must be provided in the form of the numeric internal ID. In addition, retrieving a Characteristic by name also requires specifying the name of the part, as multiple Characteristics may have the same name.

get_characteristic_by_name_response = gs_request("setup/v1/spccharacteristic/name", "GET", { "name": "Thread Pitch", "partName": "3-Inch Bolt" })
if get_characteristic_by_name_response.status_code == 200:
    characteristic_id = get_characteristic_by_name_response.json()
else:
    characteristic_id = gs_request("setup/v1/spccharacteristic", "POST", None, {
        "name": "Thread Pitch",
        "partId": part_id, # From the previously created part
        "unitId": unit_id, # From the previously created unit
        "subgroupSize": 3,
        "rangeType": "movingRange",
        "controlLimitNltZero": False,
        "displayFormat": "0.##",
        "targetX": 0.45,
        "lowerSpec": 0.449,
        "upperSpec": 0.451
    }).json()
let characteristicId;
const getCharacteristicByNameResponse = await gsRequest('setup/v1/spccharacteristic/name', 'GET', { name: 'Thread Pitch', partName: '3-Inch Bolt' });
if (getCharacteristicByNameResponse.status === 200) {
    characteristicId = await getCharacteristicByNameResponse.json();
} else {
    const createCharacteristicResponse = await gsRequest('setup/v1/spccharacteristic', 'POST', undefined, {
        name: 'Thread Pitch',
        partId: partId, // From the previously created part
        unitId: unitId, // From the previously created unit
        subgroupSize: 3,
        rangeType: 'movingRange',
        controlLimitNltZero: false,
        displayFormat: '0.##',
        targetX: 0.45,
        lowerSpec: 0.449,
        upperSpec: 0.451
    })
    characteristicId = await createCharacteristicResponse.json();
}

Create a Defect

As an exercise, create a Defect. For the purpose of this guide, only the Scratch Defect will be created, and no Defect Category is required.

Submitting Data

Using the API to collect data can be done with the Dataset endpoint. This endpoint corresponds to a collection of data collected together, as with a Sub-Inspection.

To submit data, begin by constructing the data set to be stored. This data set will include:

  • The current date
  • The Process created above
  • The Part created above
  • The Shift Traceability
  • Data for the Thread Pitch Characteristic
  • Data for the Scratch Defect

For more information about the structure of the data request object, see the data request entry in the API reference.

First, import the datetime library:

import datetime
data = {
    "processStandardId": process_id,
    "partId": part_id,
    "dateTime": datetime.datetime.now().isoformat(),
    "traceability": [
        {
            "id": traceability_id,
            "value": "Shift 1"
        }
    ],
    "data": [{
        "traceability": [],
        "type": "spc",
        "data": {
            "characteristicStandardId": characteristic_id,
            "anchorPoint": False,
            "values": [{
                "value": 0.4499
            }, {
                "value": 0.4502
            }, {
                "value": 0.4511
            }]
        }
    }, {
        "traceability": [],
        "type": "dms",
        "data": {
            "ncu": 1,
            "defects": [{
                "defectId": defect_id,
                "count": 1
            }]
        }
    }]
}
const data = {
    processStandardId: processId,
    partId: partId,
    dateTime: new Date().toISOString(),
    traceability: [
        {
            id: traceabilityId,
            value: 'Shift 1'
        }
    ],
    data: [{
        traceability: [],
        type: 'spc',
        data: {
            characteristicStandardId: characteristicId,
            anchorPoint: false,
            values: [{
                value: 0.4499
            }, {
                value: 0.4502
            }, {
                value: 0.4511
            }]
        }
    }, {
        traceability: [],
        type: 'dms',
        data: {
            ncu: 1,
            defects: [{
                defectId: defectId,
                count: 1
            }]
        }
    }]
};

Information

If you have multiple locations set in your account, you will also need to provide the locationId field as a top level property of the object.

To submit the data, issue a POST request to the Dataset endpoint, using the above constructed data as the body:

dataResponse = gs_request('dataentry/v1/dataset', 'POST', None, data)
print(dataResponse.status_code)
const dataResponse = await gsRequest('dataentry/v1/dataset', 'POST', undefined, data);
console.log(dataResponse.status);

Executing the above code should show a 200 response code, indicating that the data was inserted successfully.

Real-Time Failures

In order to check if there were any failures in the generated data set, you will first need to create a Real-Time Failure Group and attach it to the Characteristic. This may be done anywhere in the code after the creation of the Characteristic.

getRTFGroupgByNameResponse = gs_request("setup/v1/realtimefailuregroup/name", "GET", { "name": "Specs" })
if getRTFGroupgByNameResponse.status_code != 200:
    # Create the RTF Group
    rtf_group_id = gs_request("setup/v1/realtimefailuregroup", "POST", None, {
        "name": 'Specs',
        "type": 'spc',
        "addedRealTimeFailures": ["xAboveSpec", "xBelowSpec"]
    }).json()

    # Add it to the characteristic
    # Only do this on creation, otherwise it would attempt to add the same group multiple times.
    gs_request(f"setup/v1/spccharacteristic/{characteristic_id}", "POST", None, {
        "addedRTFGroups": [rtf_group_id]
    })
const getRTFGroupgByNameResponse = await gsRequest('setup/v1/realtimefailuregroup/name', 'GET', { name: 'Specs' });
if (getRTFGroupgByNameResponse.status !== 200) {
    // Create the RTF Group
    const rtfGroupResponse = await gsRequest('setup/v1/realtimefailuregroup', 'POST', undefined, {
        name: 'Specs',
        type: 'spc',
        addedRealTimeFailures: ['xAboveSpec', 'xBelowSpec']
    });
    const rtfGroupId = await rtfGroupResponse.json();

    // Add it to the characteristic
    // Only do this on creation, otherwise it would attempt to add the same group multiple times.
    await gsRequest(`setup/v1/spccharacteristic/${characteristicId}`, 'POST', undefined, {
        addedRTFGroups: [rtfGroupId]
    });
}

Now that a Real-Time Failure Group has been attached to the Characteristic, the Data Entry Real-Time Failure endpoint may be used to check for failures in a data set. Before submitting your data set, check for Real-Time Failures:

Under Construction

We're still working on this section.

Sending Alerts

Under Construction

We're still working on this section.

Analyzing Data

Now that some data has been entered, it may be analyzed. The API for retrieving data is complex, and it is recommended that data is analyzed by either exporting it as a CSV or using a Dashboard.

However, if you wish to use the API to analyze data, you may make requests to the data retrieval endpoint. The following code will retrieve the CPK statistic for the Process you created earlier:

analysisResponse = gs_request("analysis/v1/dataRetrieval", "POST", None, {
    "dataType": "spc",
    "datePeriod": {
        "type": "relative",
        "relative": "current",
        "timePeriod": "day"
    },
    "timeOffset": int(datetime.datetime.now().astimezone().utcoffset().total_seconds() / 60),
    "maxRecords": 25,
    "retrieve": [{
        "id": "myCPK",
        "type": "statsGrid",
        "stats": ["Capability"],
        "splitBy": [{ "type": "process" }]
    }],
    "standards": {
        "processCharacteristicIds": [{
            "characteristicStandardId": characteristic_id,
            "processStandardIds": [process_id]
        }]
    },
    "source": "dashboard"
}).json()

statsGridDetail = [x for x in analysisResponse["response"] if x["id"] == "myCPK"][0]
cpk = statsGridDetail["details"][0]["stats"]["cpk"]
print(f"Retrieved CPK: {cpk}")
const analysisResponse = await gsRequest('analysis/v1/dataRetrieval', 'POST', undefined, {
    dataType: 'spc',
    datePeriod: {
        type: 'relative',
        relative: 'current',
        timePeriod: 'day'
    },
    timeOffset: new Date().getTimezoneOffset() * -1,
    maxRecords: 25,
    retrieve: [{
        id: 'myCPK',
        type: 'statsGrid',
        stats: ['Capability'],
        splitBy: [{ type: 'process' }]
    }],
    standards: {
        processCharacteristicIds: [{
            characteristicStandardId: characteristicId,
            processStandardIds: [processId]
        }]
    },
    source: 'dashboard'
});

const retrievedData = await analysisResponse.json();

const statsGridDetail = retrievedData.response.find(x => x.id === 'myCPK');
console.log(`Retrieved CPK: ${statsGridDetail.details[0].stats.cpk}`);

Final Code

The full code for this sample is:

import requests
import datetime

API_KEY = "YOUR API KEY HERE"
BASE_URL = "YOUR URL"
DEVICE_ID = "MACHINE-1"

def gs_request(endpoint, method, query_params = None, body = None):
    # Establish the necessary headers
    headers = {
        "GS-Api-Key": API_KEY,
        "GS-Device-Id": DEVICE_ID
    }

    # Construct the URL
    url = f"{BASE_URL}{endpoint}"

    if method == "GET":
        # Handle GET requests, which have no body
        return requests.get(url, headers=headers, params=query_params)
    elif method == "DELETE":
        # Handle DELETE requests, which have no body
        return requests.delete(url, headers=headers, params=query_params)
    elif method == "POST":
        # Handle POST requests, and let the API know that JSON is being passed in
        headers["Content-Type"] = "application/json"
        return requests.post(url, headers=headers, json=body, params=query_params)
    else:
        raise ValueError("Invalid method type.")

def run():
    # Attempt to retrieve the process using the name as a query parameter.
    get_process_by_name_response = gs_request("setup/v1/process/name", "GET", { "name": "Welding" })

    if get_process_by_name_response.status_code == 200:
        #The Welding process was found
        process_id = get_process_by_name_response.json()
    else:
        # The process could not be found.  Create it instead.
        process_id = gs_request("setup/v1/process", "POST", None, {
            "name": "Welding",
            "sampleSize": 10,
            "ncuCost": 0.0
        }).json()

    #Traceability
    get_traceability_by_name_response = gs_request("setup/v1/trace/name", "GET", { "name": "Shift" })
    if get_traceability_by_name_response.status_code == 200:
        traceability_id = get_traceability_by_name_response.json()
    else:
        traceability_id = gs_request("setup/v1/trace", "POST", None, {
            "name": "Shift",
            "type": "text",
            "filterRTFs": False,
            "required": "never",
            "isValueSpecific": False,
            "textOptions": {
                "restricted": False
            }
        }).json()


    # Part
    get_part_by_name_response = gs_request("setup/v1/part/name", "GET", { "name": "3-Inch Bolt" })
    if get_part_by_name_response.status_code == 200:
        part_id = get_part_by_name_response.json()
    else:
        part_id = gs_request("setup/v1/part", "POST", None, {
            "name": "3-Inch Bolt",
        }).json()


    # Unit
    get_unit_by_name_response = gs_request("setup/v1/unit/name", "GET", { "name": "Inch" })
    if get_unit_by_name_response.status_code == 200:
        unit_id = get_unit_by_name_response.json()
    else:
        unit_id = gs_request("setup/v1/unit", "POST", None, {
            "name": "Inch",
        }).json()

    # Characteristic
    get_characteristic_by_name_response = gs_request("setup/v1/spccharacteristic/name", "GET", { "name": "Thread Pitch", "partName": "3-Inch Bolt" })
    if get_characteristic_by_name_response.status_code == 200:
        characteristic_id = get_characteristic_by_name_response.json()
    else:
        characteristic_id = gs_request("setup/v1/spccharacteristic", "POST", None, {
            "name": "Thread Pitch",
            "partId": part_id,
            "unitId": unit_id,
            "subgroupSize": 3,
            "rangeType": "movingRange",
            "controlLimitNltZero": False,
            "displayFormat": "0.##",
            "targetX": 0.45,
            "lowerSpec": 0.449,
            "upperSpec": 0.451
        }).json()

    getRTFGroupgByNameResponse = gs_request("setup/v1/realtimefailuregroup/name", "GET", { "name": "Specs" })
    if getRTFGroupgByNameResponse.status_code != 200:
        # Create the RTF Group
        rtf_group_id = gs_request("setup/v1/realtimefailuregroup", "POST", None, {
            "name": 'Specs',
            "type": 'spc',
            "addedRealTimeFailures": ["xAboveSpec", "xBelowSpec"]
        }).json()

        # Add it to the characteristic
        # Only do this on creation, otherwise it would attempt to add the same group multiple times.
        gs_request(f"setup/v1/spccharacteristic/{characteristic_id}", "POST", None, {
            "addedRTFGroups": [rtf_group_id]
        })

    # Defect
    get_defect_by_name_response = gs_request("setup/v1/defect/name", "GET", { "name": "Scratch" })
    if get_defect_by_name_response.status_code == 200:
        defect_id = get_defect_by_name_response.json()
    else:
        defect_id = gs_request("setup/v1/defect", "POST", None, {
            "name": "Scratch",
            "defectCost": 0.0
        }).json()

    # Data
    data = {
        "processStandardId": process_id,
        "partId": part_id,
        "dateTime": datetime.datetime.now().isoformat(),
        "traceability": [
            {
                "id": traceability_id,
                "value": "Shift 1"
            }
        ],
        "data": [{
            "traceability": [],
            "type": "spc",
            "data": {
                "characteristicStandardId": characteristic_id,
                "anchorPoint": False,
                "values": [{
                    "value": 0.4499
                }, {
                    "value": 0.4502
                }, {
                    "value": 0.4511
                }]
            }
        }, {
            "traceability": [],
            "type": "dms",
            "data": {
                "ncu": 1,
                "defects": [{
                    "defectId": defect_id,
                    "count": 1
                }]
            }
        }]
    }

    dataResponse = gs_request('dataentry/v1/dataset', 'POST', None, data)
    print(dataResponse.status_code)

    analysisResponse = gs_request("analysis/v1/dataRetrieval", "POST", None, {
        "dataType": "spc",
        "datePeriod": {
            "type": "relative",
            "relative": "current",
            "timePeriod": "day"
        },
        "timeOffset": int(datetime.datetime.now().astimezone().utcoffset().total_seconds() / 60),
        "maxRecords": 25,
        "retrieve": [{
            "id": "myCPK",
            "type": "statsGrid",
            "stats": ["Capability"],
            "splitBy": [{ "type": "process" }]
        }],
        "standards": {
            "processCharacteristicIds": [{
                "characteristicStandardId": characteristic_id,
                "processStandardIds": [process_id]
            }]
        },
        "source": "dashboard"
    }).json()

    statsGridDetail = [x for x in analysisResponse["response"] if x["id"] == "myCPK"][0]
    cpk = statsGridDetail["details"][0]["stats"]["cpk"]
    print(f"Retrieved CPK: {cpk}")


if __name__ == "__main__":
    run()
const API_KEY = 'YOUR API KEY HERE';
const BASE_URL = 'YOUR URL';
const DEVICE_ID = 'MACHINE-1';

function gsRequest(endpoint, method, queryParams, body) {
    // Establish the necessary headers.
    const headers = {
        "GS-Api-Key": API_KEY,
        "GS-Device-Id": DEVICE_ID
    };
    if (method === 'POST') {
        headers['Content-Type'] = 'application/json';
    }

    // Construct the full URL
    let url = `${BASE_URL}${endpoint}`;

    // Add URL parameters
    if (queryParams) {
        url += '?' + new URLSearchParams(queryParams).toString();
    }

    return fetch(url, {
        method: method,
        headers: headers,
        body: body ? JSON.stringify(body) : undefined,
    });
}

async function run() {
    let processId;
    // Attempt to retrieve the process using the name as a query parameter.
    const getProcessByNameResponse = await gsRequest('setup/v1/process/name', 'GET', { name: 'Welding' });

    if (getProcessByNameResponse.status === 200) {
        // The Welding process was found
        processId = await getProcessByNameResponse.json();
    } else {
        // The process could not be found.  Create it instead.
        const createProcessResponse = await gsRequest('setup/v1/process', 'POST', undefined, {
            name: "Welding",
            sampleSize: 10,
            ncuCost: 0.0
        });
        processId = await createProcessResponse.json();
    }

    // Traceability
    let traceabilityId;
    const getTraceabilityByNameResponse = await gsRequest('setup/v1/trace/name', 'GET', { name: 'Shift' });

    if (getTraceabilityByNameResponse.status === 200) {
        traceabilityId = await getTraceabilityByNameResponse.json();
    } else {
        const createTraceabilityResponse = await gsRequest('setup/v1/trace', 'POST', undefined, {
            name: 'Shift',
            type: 'text',
            filterRTFs: false,
            required: 'never',
            isValueSpecific: false,
            textOptions: {
                restricted: false
            }
        });
        traceabilityId = await createTraceabilityResponse.json();
    }

    // Part
    let partId;
    const getPartByNameResponse = await gsRequest('setup/v1/part/name', 'GET', { name: '3-Inch Bolt' });

    if (getPartByNameResponse.status === 200) {
        partId = await getPartByNameResponse.json();
    } else {
        const createPartResponse = await gsRequest('setup/v1/part', 'POST', undefined, {
            name: '3-Inch Bolt',
        });
        partId = await createPartResponse.json();
    }

    // Unit
    let unitId;
    const getUnitByNameResponse = await gsRequest('setup/v1/unit/name', 'GET', { name: 'Inch' });

    if (getUnitByNameResponse.status === 200) {
        unitId = await getUnitByNameResponse.json();
    } else {
        const createUnitResponse = await gsRequest('setup/v1/unit', 'POST', undefined, {
            name: 'Inch',
        });
        unitId = await createUnitResponse.json();
    }

    // Characteristic
    let characteristicId;
    const getCharacteristicByNameResponse = await gsRequest('setup/v1/spccharacteristic/name', 'GET', { name: 'Thread Pitch', partName: '3-Inch Bolt' });
    if (getCharacteristicByNameResponse.status === 200) {
        characteristicId = await getCharacteristicByNameResponse.json();
    } else {
        const createCharacteristicResponse = await gsRequest('setup/v1/spccharacteristic', 'POST', undefined, {
            name: 'Thread Pitch',
            partId: partId,
            unitId: unitId,
            subgroupSize: 3,
            rangeType: 'movingRange',
            controlLimitNltZero: false,
            displayFormat: '0.##',
            targetX: 0.45,
            lowerSpec: 0.449,
            upperSpec: 0.451
        })
        characteristicId = await createCharacteristicResponse.json();
    }

    // Real-Time Failure Group
    const getRTFGroupgByNameResponse = await gsRequest('setup/v1/realtimefailuregroup/name', 'GET', { name: 'Specs' });
    if (getRTFGroupgByNameResponse.status !== 200) {
        // Create the RTF Group
        const rtfGroupResponse = await gsRequest('setup/v1/realtimefailuregroup', 'POST', undefined, {
            name: 'Specs',
            type: 'spc',
            addedRealTimeFailures: ['xAboveSpec', 'xBelowSpec']
        });
        const rtfGroupId = await rtfGroupResponse.json();

        // Add it to the characteristic
        // Only do this on creation, otherwise it would attempt to add the same group multiple times.
        await gsRequest(`setup/v1/spccharacteristic/${characteristicId}`, 'POST', undefined, {
            addedRTFGroups: [rtfGroupId]
        });
    }


    // Defect
    let defectId;
    const getDefectByNameResponse = await gsRequest('setup/v1/defect/name', 'GET', { name: 'Scratch' });

    if (getDefectByNameResponse.status === 200) {
        defectId = await getDefectByNameResponse.json();
    } else {
        const createDefectResponse = await gsRequest('setup/v1/defect', 'POST', undefined, {
            name: 'Scratch',
            defectCost: 0
        });
        defectId = await createDefectResponse.json();
    }

    // Data
    const data = {
        processStandardId: processId,
        partId: partId,
        dateTime: new Date().toISOString(),
        traceability: [
            {
                id: traceabilityId,
                value: 'Shift 1'
            }
        ],
        data: [{
            traceability: [],
            type: 'spc',
            data: {
                characteristicStandardId: characteristicId,
                anchorPoint: false,
                values: [{
                    value: 0.4299
                }, {
                    value: 0.4502
                }, {
                    value: 0.4521
                }]
            }
        }]
    };

    const dataResponse = await gsRequest('dataentry/v1/dataset', 'POST', undefined, data);
    console.log(dataResponse.status);

    const analysisResponse = await gsRequest('analysis/v1/dataRetrieval', 'POST', undefined, {
        dataType: 'spc',
        datePeriod: {
            type: 'relative',
            relative: 'current',
            timePeriod: 'day'
        },
        timeOffset: new Date().getTimezoneOffset() * -1,
        maxRecords: 25,
        retrieve: [{
            id: 'myCPK',
            type: 'statsGrid',
            stats: ['Capability'],
            splitBy: [{ type: 'process' }]
        }],
        standards: {
            processCharacteristicIds: [{
                characteristicStandardId: characteristicId,
                processStandardIds: [processId]
            }]
        },
        source: 'dashboard'
    });

    const retrievedData = await analysisResponse.json();

    const statsGridDetail = retrievedData.response.find(x => x.id === 'myCPK');
    console.log(`Retrieved CPK: ${statsGridDetail.details[0].stats.cpk}`);
}

run();