Skip to main content

Basket API & Checkout

This tutorial teaches the basics of building a Javascript client that interacts with the basket.

The list of methods that can be called from the hub can be found in the API methods section.

For more information related to the initial setup and making a connection, please check the  Initial Setup & Browse API.

Basket API setup and examples

For every interaction with the basket, we need to have a basketId guid and a salesChannelId.

In our example, we store the basketId in the local storage on the client. This can also be stored on a session variable on the server side, depending on your implementation.

After the connection is established to the server, we call the getBasketId() method that first checks the urlParameters for existing basketId, then checks the local storage and if no basketId is found, we generate and save a new basketId guid to the local storage.

function getBasketId() {
// check url for basketId
const urlParams = getUrlParameters(window.location.href);
if (urlParams.basketId) {
saveBasketId(urlParams.basketId);
return urlParams.basketId;
}
// check local storage for basketId
const basketInStorage = localStorage.getItem(BASKET_KEY);
if (basketInStorage)
return basketInStorage;

// if no basketId is found, create and save a new basketId
return createBasket();

}

function saveBasketId(newId) {
basketId = newId;
localStorage.setItem(BASKET_KEY, basketId);
}

function getUrlParameters(url) {
var params = {};
var parser = document.createElement('a');
parser.href = url;
var query = parser.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
params[pair[0]] = decodeURIComponent(pair[1]);
}
return params;
}

//Generate custom basket guid
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

function createBasket() {
saveBasketId(uuidv4());
return basketId;
}
Important

Checking for existing basketId needs to be done every time when the user returns or refreshes the page or if a reconnection to the server is made because of a dropped connection.

After we have the new or existing basketId and a salesChanelId, we can then start with the basket interactions.

In the following demo, we created a simple web client that gives examples of the basket API methods calls and prints the received response messages and objects.

BasketAPI.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Basket API Client</title>

<!-- JS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"></script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/5.0.13/signalr.min.js" integrity="sha512-IS7oxIOouU8HW/57DUnTJd9AQ9nzCfdTKmYuZtYcCHaKbdS/1f3Sce+eR5qkrqMmYuGD6UE4pcutXuzgB6kGRQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

<!-- CSS -->
<link rel="stylesheet" href="assets/css/theme.css">
</head>
<body>
<div id="wrapper" style="margin:30px;">
<h1>Basket API examples</h1>
<br />
<form>
<div class="form-group">
<label for="txtSalesChannelId">Sales Channel ID:</label>
<input type="text" class="form-control" id="txtSalesChannelId" placeholder="Enter sales channel Id">
<br />
<p>Example salesChannelId: 680cbfbc-76a6-cb30-4c04-39b26251f870</p>
<br />
</div>
<button type="submit" id="btnCreateBasket" class="btn btn-primary">Create Basket</button>
<button type="submit" id="btnResumeShopping" class="btn btn-primary">Resume Shopping</button>
<button type="submit" id="btnAddItem" class="btn btn-primary">Add Item</button>
<button type="submit" id="btnRemoveItems" class="btn btn-primary">Remove Items</button>
<button type="submit" id="btnApplyPromoCode" class="btn btn-primary">Apply Promo Code</button>
<button type="submit" id="btnRemovePromoCode" class="btn btn-primary">Remove Promo Code</button>
<br />
<br />
<button type="submit" id="btnResetBasket" class="btn btn-primary">Reset Basket</button>
</form>
<br /><br />
<h2 id="lblResponse">Responses:</h2>
<div>
<ul id="response" class="list-group"></ul>
</div>
</div>

<script src="assets/js/BasketAPI.js"></script>
</body>
</html>
</body>
</html>

This is the complete BasketAPI.js demo code:

BasketAPI.js
document.addEventListener("DOMContentLoaded", () => {
const connection = new signalR.HubConnectionBuilder()
.withUrl("https://api.skchase.com/checkout")
.configureLogging(signalR.LogLevel.Information)
.build();

const ERROR_CODES = {
ACTIVE_PROMO_CODE_NOT_FOUND: 'ActivePromoCodeNotFound',
PROMO_CODE_DOESNT_EXIST: 'PromoCodeDoesntExist',
ORDER_PLACEMENT_TIMEOUT: 'OrderPlacementTimeout',
STALE_BASKET: 'StaleBasket',
UNAVAILABLE_PRODUCT: 'UnavailableProduct'
};

var BASKET_KEY = 'basket-identifier';
var basketId = getBasketId();
var salesChannelId = document.getElementById("txtSalesChannelId").value;

var itemsToRemove = [];

document.getElementById("btnCreateBasket").disabled = true;
document.getElementById("btnResumeShopping").disabled = true;
document.getElementById("btnAddItem").disabled = true;
document.getElementById("btnRemoveItems").disabled = true;
document.getElementById("btnApplyPromoCode").disabled = true;
document.getElementById("btnRemovePromoCode").disabled = true;
document.getElementById("btnResetBasket").disabled = true;
document.getElementById("lblResponse").hidden = true;

function getBasketId() {
// check url for basketId
const urlParams = getUrlParameters(window.location.href);
if (urlParams.basketId) {
saveBasketId(urlParams.basketId);
return urlParams.basketId;
}
// check local storage for basketId
const basketInStorage = localStorage.getItem(BASKET_KEY);
if (basketInStorage)
return basketInStorage;

// if no basketId is found, create and save a new basketId
return createBasket();
}

function saveBasketId(newId) {
basketId = newId;
localStorage.setItem(BASKET_KEY, basketId);
}

function getUrlParameters(url) {
var params = {};
var parser = document.createElement('a');
parser.href = url;
var query = parser.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
params[pair[0]] = decodeURIComponent(pair[1]);
}
return params;
}

// Generate custom basket guid
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

function createBasket() {
saveBasketId(uuidv4());
return basketId;
}

// Handle response messages
connection.on("BasketUpdated", (model) => {
basketId = model.basketId;
var node = document.createElement("LI");
node.className = "list-group-item ";
var textnode = document.createTextNode(JSON.stringify(model));
node.appendChild(textnode);
document.getElementById("response").appendChild(node);

// we take the lineItemId from the first item element as an example for RemoveItems() call
if (model && model.items.length > 0) {
itemsToRemove.push(model.items[0].lineItemId);
}
console.log("Basket Updated!");
console.log(model);
});

connection.on("SomethingHappened", (error) => {
console.warn('Something happened: ', error);
appendMessage(error.errorCode);
switch (error.errorCode) {
case ERROR_CODES.ACTIVE_PROMO_CODE_NOT_FOUND:
case ERROR_CODES.PROMO_CODE_DOESNT_EXIST:
console.warn('promoCodeInvalid');
break;
default:
if (!_.contains(SUPPRESSED_ERROR_CODES, error.errorCode)) {
console.warn('unhandledError', error);
}
console.warn('Error received from server: ', error.errorCode);
break;
}
});

// Invoke Hub Methods
// Create Basket
document.getElementById("btnCreateBasket").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
await connection.invoke("CreateBasket", basketId, salesChannelId).then(function () {
var text = "create basket called";
appendMessage(text);
});
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

// Resume Shopping
document.getElementById("btnResumeShopping").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
await connection.invoke("ResumeShopping", basketId, salesChannelId).then(response => {
if (response.error != null) {
if (response.error === ERROR_CODES.STALE_BASKET) {
createBasket();
} else {
console.warn('Error received from server: ', response.error);
}
return;
}

var text = "resume shopping called";
appendMessage(text);
});
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

// Add Item
document.getElementById("btnAddItem").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
// Parameter voucher's guid
await connection.invoke("AddItem", "1eaf0000-0000-0000-0000-000000004167")
.then(function () {
var text = "add item called";
appendMessage(text);
});
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

// Remove Items
document.getElementById("btnRemoveItems").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
// Parameter voucher's guid
if (itemsToRemove.length > 0) {
await connection.invoke("RemoveItems", itemsToRemove)
.then(function () {
var text = "remove items called";
appendMessage(text);
});
}
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

// Apply Promo Code
document.getElementById("btnApplyPromoCode").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
await connection.invoke("ApplyPromoCode", "HOTEL21")
.then(function () {
var text = "apply promo code called";
appendMessage(text);
});
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

// Remove Promo Code
document.getElementById("btnRemovePromoCode").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
await connection.invoke("RemovePromoCode")
.then(function () {
var text = "remove promo code called";
appendMessage(text);
});
}
catch (err) {
console.log(err);
var text = "remove promo code failed - reason: " + err.errorCode;
appendMessage(text);
}
}
}
event.preventDefault();
});

// Reset Basket
document.getElementById("btnResetBasket").addEventListener("click", event => {

salesChannelId = document.getElementById("txtSalesChannelId").value;

if (salesChannelId) {
invoke();
async function invoke() {
try {
createBasket()
await connection.invoke("CreateBasket", basketId, salesChannelId).then(function () {
var text = "basket is reset and new basketId is created";
appendMessage(text);
});
}
catch (err) {
console.log(err);
}
}
}
event.preventDefault();
});

function appendMessage(text) {
var node = document.createElement("LI");
node.className = "list-group-item ";
var textnode = document.createTextNode(text);
node.appendChild(textnode);
document.getElementById("response").appendChild(node);
}

async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
}
};

connection.onclose(start);

// Start the connection.
start().then(function () {
document.getElementById("btnCreateBasket").disabled = false;
document.getElementById("btnResumeShopping").disabled = false;
document.getElementById("btnAddItem").disabled = false;
document.getElementById("btnRemoveItems").disabled = false;
document.getElementById("btnApplyPromoCode").disabled = false;
document.getElementById("btnRemovePromoCode").disabled = false;
document.getElementById("btnResetBasket").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});;
});

In the example code, except the calls to the basket API methods, there are two handler methods that listen for "BasketUpdated" and "SomethingHappened" messages from the server.
"BasketUpdated" is sent from the server after each basket interaction and "SomethingHappened" in case of an error. See the list here.

These are the hub methods that we invoke from our demo javascript code:

  • CreateBasket()")
  • ResumeShopping()")
  • AddItem()")
  • RemoveItems()")
  • ApplyPromoCode()")
  • RemovePromoCode()")

There is also a method called ResetBasket() that generates a new basketId and calls the CreateBasket() method to start the tracking of the new basket.

Checkout

After creating and interacting with the basket, in order to complete an order, we first need to redirect to the checkout and start the checkout flow.

The link to redirect to would be the domain of your site (provided from SKChase) ending with /checkout, together with the following query string parameters: basketId, returnUrl, successCallbackUrl.

// Example URL
'yourdomain'/checkout?basketId=ec7f3c4e-1c09-4e75-8c15-ad3578c21977&returnUrl=/urlBackToYourSite&successCallbackUrl=/successCallback
  • basketId is the guid of the current basket that contains the items to order. The basketId will be used until the order is completed or the basket is reset.
  • returnUrl will be used to return back to your site before or after an order is completed. After a successful purchase, there will be a 'Continue Shopping' button that will take the user back to your site;
  • successCallbackUrl will be called  after the checkout is successfully completed. The orderId is sent as a parameter.

To start a new shopping session, you'll need to generate a new basketId and start again the process.