Documentation

Integration Guide

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

Using correct endpoints for integration

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

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.pull 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.

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.

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 delivery 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's completely unnecessary when receiving 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.

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.pull 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.pull` 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 category 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.pull.

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

summary_changed

Useful when utilising Address Form and Delivery Groups features. If you don't have those enabled, you want to go with the Data Changed event. Exposes two arguments:

data: {
// Summarised value of all delivery groups
total_value: number;
// Delivery address provided by the user in the Delivery Checkout
delivery_address: Recipient | undefined;
// Billing address provided by the user in the Delivery Checkout
billing_address: Recipient | undefined;
}
meta: {
total_value_changed: boolean;
delivery_address_changed: boolean;
billing_address_changed: boolean;
}

You can see detailed breakdown of the Recipient here

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;
category_name: string;
pickup_location?: {
address: {
country: string;
postal_code: string;
address_lines: string[];
city: string;
coordinates: Coordinates;
};
name: string;
}
}
meta: {
delivery_type_changed: boolean;
external_method_id_changed: boolean;
price_changed: boolean;
search_address_changed: boolean;
shipping_method_changed: boolean;
initial_load: boolean;
category_name_changed: boolean;
pickup_location_changed: 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. If Address Form is used, the data will consists of Recipient object.

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

// Address Form scenario
window._sw(function (api) {
api.on("loaded", function (recipient) {
console.log("loaded");
doSomethingWithAddress(recipient);
});
});

no_shipping_options

Emitted when the address is provided but Ingrid can't find any category 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");
});
});

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 that runs 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.

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/delivery_checkout/session.create' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer your-site-token' \
--data-raw '{
"purchase_country": "SE",
"purchase_currency": "SEK",
"locales": [
"sv-SE"
],
"search_address": {
"postal_code": "11239",
"country": "SE"
},
"cart": {
"cart_id": "unique_id",
"total_value": 129900,
"currency": "SEK",
"items": [
{
"sku": "SKU12345",
"name": "Saucony Shadow 6000",
"quantity": 1,
"price": 1000
}
]
},
"external_id": "your-order-id"
}'

Last updated: Wed, Nov 20, 06:15 AM