Documentation

Integration Guide

This part of the documentation will guide you through the steps of integrating your e-commerce site with SIW. We'll go through patterns for embedding the shipping selector, handle updates to cart or customer information and marking the checkout session as completed in SIW API.

Using correct endpoints for integration

Make sure that you are using correct API endpoint when using different environments for testing and production releases.

Note

Please note that we reserve the right to introduce additional fields to our API responses without prior notification. Users are encouraged to regularly check for updates in our documentation for any changes that may impact their integration.

Creating a session

The first thing you need to do before you can embed the shipping selector in your checkout page is to create a session. This is done by calling session.create which returns session details and an HTML snippet.

Each Session has an ID that can be used to later fetch, update or complete the session. It is recommended that you store the session id together with the customer's order.

Embed the Delivery Checkout

The responses of session.create and session.get contain the attribute called html_snippet which includes the html snippet to display the shipping selector in your checkout page. Just put it in the part of the page where you want the Delivery Checkout to be displayed.

The snippet will then download and execute our bootstrap.js script which spawns the iframes and sets up channels of communication for session management.

Live React examples

All code examples come from our live React code sandbox. You can copy the snippets straight from there 📋

const handleCreateSession = async () => {
try {
// Call your backend and they call siw/session.create
const sessionResponse = await createSession();
// Embed the widget on the page
widgetWrapperRef.current.innerHTML = HTMLSnippet;

// Trick the browser to run the script, explained below
replaceScriptNode(document.getElementById("shipwallet-container"));
} catch {
// We don't believe it's possible but if our service fails, you render the fallback for the widget here
}
};
Warning

If you're dynamically inserting the HTML snippet in the DOM, most browsers won't execute the script embedded in it. If you're lucky enough to have jQuery on your page, you can use its append() function or feel free to copy the snippet below. It removes and inserts back the script nodes to trick the browser to execute them

function replaceScriptNode(node) {
if (isScriptNode(node) && !isExternalScript(node)) {
node.parentNode.replaceChild(cloneScriptNode(node), node);
} else {
var i = 0,
children = node.childNodes;
while (i < children.length) {
replaceScriptNode(children[i++]);
}
}
return node;
}

function isScriptNode(node) {
return node.tagName === "SCRIPT";
}

function isExternalScript(node) {
return !!node.src && node.src !== "";
}

function cloneScriptNode(node) {
var script = document.createElement("script");
script.text = node.innerHTML;
for (var i = node.attributes.length - 1; i >= 0; i--) {
script.setAttribute(node.attributes[i].name, node.attributes[i].value);
}
return script;
}

replaceScriptNode(document.getElementById("shipwallet-container"));

Caveats

The HTML snippet is designed to only be added to the page once. So if you are using AJAX to load the snippet you should make sure that this only happens once. Adding or replacing it multiple times will cause errors.

However, there is an exception to that when using Multiple widgets. It allows you to add many checkout widgets to the page, but each has different id and in that case, the above constraint applies to each unique widget.

Warning

html_snippet contains an element with unique id shipwallet-container. Make sure there’s no other element on the page with the same ID, otherwise the widget won't load.

You can override the default unique id with your own by requesting a merchant configuration change from our Customer Support.

Updating the cart and session

Checkout pages often allow the user to alter the contents of the cart, locale, customer information or anything else that the session is based on. Change in any of those may adjust the available shipping options or the visual apperance of the widget

In such case, you will need to update the session to inform Ingrid about the changes. However, you will also have to suspend the widget, making it non-interactive for the time of the request, and resume it after you get a response from our API. This allows the widget to synchronise it's state with the backend service.

Warning

Suspending and resuming the widget too often will cause a poor experience for the user.

It is completely unnecessary when reacting to user events.

You only want to do this if you made an api call towards update session as a result of a user interaction on your website affecting the session arguments

Two methods are exposed on the window._sw object.

const handleUpdateSession = async () => {
window._sw((api) => api.suspend());
await updateSession(session.id);
window._sw((api) => api.resume());
};

Handling abandoned carts

In some cases customers can drop out of the checkout process early. Either because they didn't have time to complete the purchase or they went on to continue shopping on the page or for some other reason. In cases like this, when the customer returns, we want them to continue where they left off. By storing the session ID, we can fetch an existing session instead of creating a new one.

This might be easier to follow in our live demo. Here's how it could be handled:

React.useEffect(() => {
initDeliveryCheckout();
}, []);

const initDeliveryCheckout = async () => {
const existingSessionID = window.localStorage.getItem("ingrid-session-id");
if (existingSessionID) {
const getSessionResponse = await getSession(existingSessionID);
handleIngridInitialize(getSessionResponse.data.session, getSessionResponse.data.html_snippet);
} else {
handleCreateSession();
}
};

const handleIngridInitialize = (session: SIWSessionDTO, HTMLSnippet: string) => {
// session.get and session.create should be handled the same way. We've created this utility function
// to abstract adding the snippet to the page and setting up event listeners without doubling the code
widgetWrapperRef.current!.innerHTML = HTMLSnippet;
replaceScriptNode(document.getElementById("shipwallet-container"));

window._sw!((api) => {
api.on("data_changed", (data, meta) => {
if (meta.price_changed) {
setShippingPrice(data.price);
}
});
});
};

const handleCreateSession = async () => {
try {
const sessionResponse = await createSession();
handleIngridInitialize(sessionResponse.data.session, sessionResponse.data.html_snippet);

// You want to save the session ID somewhere, so when the user abandons the checkout
// you can later restore the session, instead of creating a new one
window.localStorage.setItem("ingrid-session-id", sessionResponse.data.session.id);
} catch {}
};
Note

If, for some reason, the session cannot be found when you try to call session.update or session.get with a previous session ID we suggest that you handle this error in this case by creating an entirely new session.

Subscribing to user events

User can perform a number of actions that can affect your checkout, e.g. user changing the shipping option can cause a shipping price change. Luckily, the moment the widget attaches itself to the page, it sets up a Javascript API available via window._sw where you can set up listeners for events.

You would usually do this right after session.create or session.get. This is how we handle it in our React example

const handleCreateSession = async () => {
// ...call session create backend and add the snippet to the page

// Update the shipping price any time option is changed
window._sw(function (api) {
api.on("data_changed", function (data, meta) {
if (meta.price_changed) {
updateCart(data.price);
}
});
});
};

Available events

data_changed

Instead of exposing multiple 'change' events with multiple properties, Delivery Checkout exposes one 'change' event and allows you to decide what data you care about. The callback receives two arguments

data: {
delivery_type: 'delivery' | 'mailbox' | 'pickup' | 'instore';
price: number;
search_address: {
country: string;
postal_code: string;
address_lines?: string[];
city?: string;
region?: string;
};
shipping_method: string;
external_method_id?: string;
}

and

meta: {
delivery_type_changed: boolean;
external_method_id_changed: boolean;
price_changed: boolean;
search_address_changed: boolean;
shipping_method_changed: boolean;
initial_load: boolean;
}

meta.initial_load: true informs you that this is the moment where values are actually set, not updated, as this is the first render of the widget.

All that looks kind of complicated, how does one use it?

Say you want to know when the shipping price changes to update the total price on the cart

window._sw(function (api) {
api.on("data_changed", function (data, meta) {
if (meta.price_changed) {
updateCart(data.price);
}
});
});

If you want to also know when the shipping method changes

window._sw(function (api) {
api.on("data_changed", function (data, meta) {
if (meta.shipping_method_changed) {
sendToAnalyticsService(data.shipping_method);
}
if (meta.price_changed) {
updateCart(data.price);
}
});
});

You can have more if statements or setup multiple data_changed event listeners if that's your jam.

loaded

Emitted when the widget is fully loaded and interactive

window._sw(function (api) {
api.on("loaded", function () {
console.log("loaded");
});
});

no_shipping_options

Emitted when the address is provided but Ingrid can't find any shipping options for it. Note that this requires additional configuration on Ingrid side to be visible. To do that, you will have to contact our support team

window._sw(function (api) {
api.on("no_shipping_options", function () {
console.log("no shipping options");
});
});
Note

If you render multiple widgets simultaneously, then global variable for event subscription is not called _sw, see Multiple widgets.

Completing session

You should complete the session after receiving confirmation from payment system. It marks the session as complete and creates a Transport Order in the Ingrid system.

If customer address is provided during session completion and it differs from the search address used during the session lifetime, shipping options might get recalculated. The recalculation may happen when the selected shipping option was a home delivery one or the search address contained neither a country nor a postal code. Search address drives the shipping option generation during the session lifetime, but can be overridden under those conditions during session complete.

Tip

When issuing the complete session request, include the external order ID if possible. You can find more information about external order ID in the subsection using external order identifier.

Using external order identifier

It is highly encouraged to pass external order identifier (external ID for short) in your session create/update/complete requests. Passing it in one of those requests will store the external ID along with the other session data.

Note

Benefits of using external ID are:

  • being able to search by your external order ID in Ingrid Merchant Platform transport orders,
  • when booking with Ingrid, your external order ID will be propagated to the booking system,
  • being able to register and load the tracking widget using the external order ID provided by the merchant.

You can pass the external_id field as part of your session create/update/complete request. The following snippet shows how you can pass it in the request via curl.

curl --location --request POST 'https://api.ingrid.com/v1/siw/session.create' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your-site-token' \
--data-raw '{
"purchase_country": "SE",
"purchase_currency": "EUR",
"locale": "en-US",
"cart": {
"total_value": 1000,
"currency": "EUR",
"items": [
{
"sku": "a3ec72d0-836f-4668-9c54-5c86d67ca897",
"name": "Keyboard",
"quantity": 1,
"price": 1000
}
]
},
"external_id": "your-order-id"
}'

Last updated: Thu, Mar 28, 06:15 AM