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.
Make sure that you are using correct API endpoint when using different environments for testing and production releases.
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.
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
}
};
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"));
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.
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.
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.
- suspend - Puts the widget in a freezed state with a spinner covering the whole UI
- resume - Unlocks the widget, triggering an internal BE request for synchronization
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 {}
};
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");
});
});
If you render multiple widgets simultaneously, then global variable for event subscription is not
called _sw
, see Multiple widgets.
error
The event is emitted when an unexpected error occurs — for example, it could be network or authorisation error. Error object consists of type
and severity
fields.
window._sw((api) =>
api.on("error", (data) => {
console.log("error", data); // { type: 'missing_session_token', severity: 'fatal' }
})
);
destroy
This method is useful when you want to reinitialize Checkout Widget. This API method accepts a callback which is ran after the Checkout Widget cleanup is complete.
window._sw((api) =>
api.destroy(() => {
handleInitializeCheckoutWidget();
})
);
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.
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.
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"
}'