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

# 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 (opens new window). 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

It is common for a checkout page to contain customer interactions that can alter the customer's cart or contact information. Two parts that are used by the shipping selector to display the correct shipping options for a customer. Maybe a customer can login on your checkout page, or maybe they add or remove items from their cart.

In these cases you must use the session.update call to update the session with the new information. However, you will have to suspend and resume the widget during this process to sync the frontend with the backend. This allows the shipping options to be refreshed with up to date information about the cart and responses from carriers. It is essential that you do it after each session update as it might cause inconsistencies between the UI and the actual session.

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 (opens new window). 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("shipping_option_changed", (option) => {
      setShippingPrice(option.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 (opens new window)

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-development.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: 11/23/2021, 9:15:27 AM