DEVELOPER

Back to Developer Blog

technicalseries

Getting Started with Embedded Payments in a Vue.js App (Developer Guide)

By Arek Nawo and Laura Olson | October 20th, 2024

The ability for an ecommerce business to accept payments online depends on the quality of the software that handles the transaction. A key way to create the best user experience in this area is with an embedded payments solution. It allows you to create a customized checkout experience where you have complete control over how the checkout works and how it integrates with your website's design and functionality. You'll also benefit from features of the solution, such as PCI compliance and data security, and support for a variety of payment methods and credit card types handled by your payment gateway.

alt

In this article, you'll learn how to create a custom checkout experience using North's iFrame JavaScript SDK in a Vue.js application. While the SDK allows you to create an entirely custom checkout system from the ground up, North handles the sensitive payment information to ensure your solution is secure and PCI compliant.

Throughout this tutorial, you can follow along with this GitHub repo.

Build this App

Clone this code repository to quickly create your own app today!

Implementing Embedded Payments with Vue.js

In this tutorial, you'll create a simple, full-stack Vue.js app that handles both the front-end and back-end aspects of the payment process. For that, you'll not only use Vue components but also Nuxt 3 — a full-stack app framework built on top of Vue.

Prerequisites

Make sure you have Node.js v16.10.0 or newer installed. Start by creating a new project using Nuxi (the Nuxt CLI) and initiating the development server:

npx nuxi init project
cd project
npm install
npm run dev

After running this code, when you open localhost:3000, you will see Nuxt project's default page. This means that the project has been successfully created and the dev server is running.

Setting Up a North Account

Before getting into the code, you should first get credentials for accessing the North iFrame JS SDK.

If you haven't already, start by creating a North account. Then check out the official iFrame JS SDK integration guide — especially the server security requirements you'll have to meet in your production environment.

To get test credentials, contact the North support team using the contact form.

Once you have access to your API keys, you need to securely add them to your project. Start by updating the nuxt.config.ts file, as follows:

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      mid: "",
      gatewayPublicKey: "",
    },
    xNabwssAppsource: "",
    developerKey: "",
    password: "",
  },
});

This configuration ensures that you'll be able to access the test credentials through the Nuxt API. Variables placed within public will be available on both the front-end and back-end, while those outside will only be available on the back-end.

To provide values for the variables, place your credentials in the .env file:

NUXT_PUBLIC_MID=
NUXT_PUBLIC_GATEWAY_PUBLIC_KEY=
NUXT_X_NABWSS_APPSOURCE=
NUXT_DEVELOPER_KEY=
NUXT_PASSWORD=

Get in Touch

Talk to us about creating an ecommerce store using Vue today.

Creating the Product Page

Now that you have access to the credentials, you can start creating the product page. Inside the app.vue file, add the following code:

HTML
<template><NuxtPage /></template>
<style>
  :root {
    font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
    line-height: 1.5;
    font-weight: 400;
    font-synthesis: none;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    -webkit-text-size-adjust: 100%;
    color: #213547;
    background-color: #ffffff;
  }
  body {
    margin: 0;
    display: flex;
    place-items: center;
    min-width: 320px;
    min-height: 100vh;
  }
  h1 {
    font-size: 3.5rem;
    line-height: 1.1;
  }
  h2 {
    font-size: 1.5rem;
    font-weight: 600;
    opacity: 0.8;
    line-height: 1.1;
  }
  button {
    border-radius: 8px;
    border: 1px solid transparent;
    padding: 0.6rem 1.2rem;
    font-size: 1rem;
    font-weight: 500;
    font-family: inherit;
    background-color: #1a1a1a;
    color: #ffffff;
    cursor: pointer;
  }
  button:hover {
    outline: none;
    background-color: #000000;
  }
</style>

The app.vue component serves as an entry to your Vue app, so it's a good place to set up global styles. In the template, make sure to include the NextPage component, which is responsible for rendering the current route.

Now, create a new component representing a new page in pages/product.vue:

HTML
<script setup>
  const amount = 1.99;
</script>

<template>
  <div class="container">
    <div class="image-container">
      <img src="../assets/vue.svg" class="image" alt="Vue logo" />
    </div>
    <div class="description-container">
      <div class="description">
        <h1 class="heading">Awesome Vue Sticker</h1>
        <h2 class="heading">${{ amount }} USD</h2>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. In placerat
          magna enim, vel blandit lacus elementum quis. Etiam elementum ligula
          venenatis consequat gravida. Phasellus orci nibh, elementum sit amet
          pharetra in, elementum sit amet ipsum.
        </p>
        <button>Purchase</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
  .container {
    display: flex;
    height: 100vh;
    width: 100vw;
    padding: 2rem;
    flex-direction: column;
    box-sizing: border-box;
  }
  .image-container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    flex: 1;
  }
  .description-container {
    flex: 1;
    display: flex;
    justify-content: center;
    flex-direction: column;
    align-items: center;
    text-align: start;
  }
  .heading {
    margin: 0;
  }
  .description {
    max-width: 28rem;
  }
  .image {
    flex: 2;
    padding: 1.5rem;
    will-change: filter;
    transition: filter 300ms;
    height: 100%;
    box-sizing: border-box;
    object-fit: contain;
  }
  .image:hover {
    filter: drop-shadow(0 0 2rem #42b883aa);
  }
  @media (min-width: 768px) {
    .container {
      flex-direction: row;
      gap: 4rem;
      padding: 4rem;
    }
    .image-container {
      flex: 2;
    }
  }
</style>

The code above gives you a simple, styled page thanks to the included CSS. It contains an image of the product and most importantly, the payment button.

You can preview the product page at the localhost:3000/product/ route, which corresponds with the component's file name.

Next, you'll create the modal that the user should be presented with when they click the Purchase button. For this, create a separate file — components/payment-modal.vue — for a new component with the following code:

HTML
<template>
  <div class="container" :class="opened ? '' : 'hidden'">
    <div class="overlay" @click="$emit('closeModal')"></div>
    <div class="modal">
      <h2 style="display: flex; justify-content: center; align-items: center">
        <span style="flex: 1">Payment</span
        ><span class="price">${{ amount }} USD</span>
      </h2>
      <form id="pay">
        <label>Full Name:</label>
        <div id="full-name" class="form-field">
          <input name="fullName" />
        </div>
        <div class="horizontal" style="width: 100%">
          <div class="vertical" style="flex: 3">
            <label>Card Number:</label>
            <div id="card-number" class="form-field"></div>
          </div>
          <div class="vertical" style="flex: 1">
            <label>CVV:</label>
            <div id="card-cvv" class="form-field"></div>
          </div>
        </div>
        <div class="vertical">
          <label>Street:</label>
          <div id="address" class="form-field"></div>
        </div>
        <div class="horizontal">
          <div class="vertical">
            <label>Zip:</label>
            <div id="zipFirst5" class="form-field"></div>
          </div>

          <div class="vertical">
            <label>Zip+4:</label>
            <div id="zipPlus4" class="form-field"></div>
          </div>
        </div>
        <input type="hidden" id="card-token" />
        <input
          type="button"
          @click="submitPayment"
          name="tokenize_and_pay"
          class="submit-button"
          value="Submit Payment"
        />
      </form>
      <div class="centered"><span id="alert_message"></span></div>
    </div>
  </div>
</template>
<script setup>
  const config = useRuntimeConfig();
  const props = defineProps({
    opened: Boolean,
    amount: Number,
  });
  const emit = defineEmits(["closeModal"]);
  const submitPayment = async () => {};
</script>
<style scoped>
  .container {
    display: flex;
    justify-content: center;
    align-items: center;
    position: fixed;
    height: 100vh;
    width: 100vw;
    top: 0;
    left: 0;
  }
  .price {
    font-size: 0.8rem;
    font-weight: 600;
    opacity: 0.8;
    line-height: 1.1;
  }
  .horizontal {
    display: flex;
    flex-direction: row;
    gap: 1rem;
    width: 100%;
  }
  .centered {
    display: flex;
    justify-content: center;
    align-items: center;
  }
  .vertical {
    display: flex;
    flex-direction: column;
  }
  .overlay {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    transition: all 0.3s ease-in-out;
  }
  .container.hidden {
    opacity: 0;
    pointer-events: none;
  }
  .modal {
    background-color: white;
    padding: 2rem;
    border-radius: 1rem;
    display: flex;
    width: calc(100% - 6rem);
    max-width: 20rem;
    flex-direction: column;
    position: absolute;
  }
  .modal h2 {
    margin-top: 0px;
  }
  .form-field {
    height: 3rem;
    width: 100%;
  }
  .form-field input {
    border: 0;
    background: #f1f5f9;
    border-radius: 8px;
    color: #000000;
    width: calc(100% - 1rem);
    height: 1rem;
    padding: 0.75rem 0.5rem;
    font-size: 1rem;
  }
  .form-field input:focus {
    outline: none;
  }
  .submit-button {
    border-radius: 8px;
    border: 1px solid transparent;
    padding: 0.6rem 1.2rem;
    margin-top: 1rem;
    font-size: 1rem;
    font-weight: 500;
    font-family: inherit;
    background-color: #3fb883;
    color: #ffffff;
    cursor: pointer;
    transition: border-color 0.25s;
    width: 100%;
  }
  .submit-button:hover {
    background-color: #329368;
  }
  #alert_message {
    text-align: center;
    max-width: 14rem;
    font-size: 0.8rem;
    margin-top: 1rem;
  }
</style>

The modal contains a set of input fields and is controlled through the props. You can see that some fields have input elements while others are empty div elements. That's because the latter will be used for holding iframe elements provided and loaded by the SDK. That's also why you should keep the IDs of the elements in mind — they will be important later when you integrate with the iFrame SDK.

Also take note that submitting the form calls the submitPayment() function. However, it will remain empty until the SDK is integrated.

To add the PaymentsModal component to the page, you'll have to make a few edits to pages/product.vue:

HTML
<script setup>
  import PaymentsModal from "@/components/payment-modal.vue";

  const paymentModalOpened = ref(false);
  const amount = 1.99;
</script>

<template>
  <div class="container">
    <div class="image-container">
      <img src="../assets/vue.svg" class="image" alt="Vue logo" />
    </div>
    <div class="description-container">
      <div class="description">
        <h1 class="heading">Awesome Vue Sticker</h1>
        <h2 class="heading">${{ amount }} USD</h2>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipiscing elit. In placerat
          magna enim, vel blandit lacus elementum quis. Etiam elementum ligula
          venenatis consequat gravida. Phasellus orci nibh, elementum sit amet
          pharetra in, elementum sit amet ipsum.
        </p>
        <button @click="paymentModalOpened = true">Purchase</button>
      </div>
    </div>
  </div>
  <PaymentsModal
    :opened="paymentModalOpened"
    :amount="amount"
    @close-modal="paymentModalOpened = false"
  />
</template>

Thanks to the script setup syntax, you only need to import a component to use it. You can then use it by binding its props and event handlers to specific values, like a newly created paymentModalOpened ref for the opened prop, to control the opening state of the modal. The ref() function, like many other functions and utilities provided by Vue.js or Nuxt, is auto-imported, which means you don't have to explicitly specify an import.

The modal is still missing some styling and most importantly, functionality. It's time to fix that with North's iFrame JS SDK.

Integrating the iFrame JS SDK

To start working with the iFrame JS SDK, you should first load the https://sdk.paymentshub.dev/pay-now.min.js module in your head element. To do so, use the useHead() composable provided by Nuxt. In pages/product.vue, add the following code inside the script setup block:

HTML
<script setup>
  import PaymentsModal from "@/components/payment-modal.vue";

  const paymentModalOpened = ref(false);
  const amount = 1.99;

  useHead({
    title: "Awesome Vue Sticker",
    script: [{ src: "https://sdk.paymentshub.dev/pay-now.min.js" }],
  });
</script>

useHead() alters the content of the head element for the page it's used in. In the code above, it adds a custom title and script tag to load the SDK.

With the SDK loaded, you now have access to a PayNow global object.

Next, create a utils/sdk.js module in which you will place all the utility functions for interacting with the SDK:

const initializeSDK = (mid, gateway_public_key) => {
  const PayNowSdk = PayNow.default;
  const options = {
    cardFieldId: "card-number",
    cvvFieldId: "card-cvv",
    addressFieldId: "address",
    zipFieldId: "zipFirst5",
    zipPlusFourFieldId: "zipPlus4",
  };

  PayNowSdk().on("ready", () => {
    const fieldStyling =
      "border:0;background:#f1f5f9;border-radius:8px;width:calc(100% - 1rem);height:100%;padding:0.75rem 0.5rem;";
    const numberStyling = fieldStyling;
    const cvvStyling = fieldStyling;
    const streetStyling = fieldStyling;
    const zipStyling = fieldStyling;
    const zip4Styling = fieldStyling;

    PayNowSdk().setStyle("number", numberStyling);
    PayNowSdk().setStyle("cvv", cvvStyling);
    PayNowSdk().setStyle("address", streetStyling);
    PayNowSdk().setStyle("zip", zipStyling);
    PayNowSdk().setStyle("zipPlusFour", zip4Styling);
    PayNowSdk().setNumberFormat("prettyFormat");
  });
  PayNowSdk().init(gateway_public_key, mid, options);
};

export { initializeSDK };

The first one of these utility functions is initializeSDK, which, with proper credentials, will initialize the SDK along with all the sensitive fields like card number or CVV inputs. As you can see, it does so by referencing the container div elements from the payment modal by their IDs. On top of that, it allows you to style the iframe-based inputs using inline styles. This functionality is used to complete the design of the payment modal.

To call this function and initialize the SDK, back inside the components/payment-modal.vue file, add the following code inside the script setup block:

HTML
<script setup>
  const config = useRuntimeConfig();
  const props = defineProps({
    opened: Boolean,
    amount: Number,
  });
  const emit = defineEmits(["closeModal"]);
  const submitPayment = async () => {};

  onMounted(() => {
    initializeSDK(config.public.mid, config.public.gatewayPublicKey);
  });
</script>

Nuxt auto-imports all the functions from the utils folder that it notices you use. This allows you to use initializeSDK() without any additional imports.

The SDK is initialized inside the onMounted() lifecycle hook, which is run after the UI, including the form fields, has already been mounted and rendered.

Also note the use of the useRuntimeConfig() composable. It gives you access to the credentials configured earlier, which means you can now easily access and forward the credentials to the initializeSDK(). Since you are accessing a public config property, you need to prefix it with .public — e.g., config.public.mid.

With the payments modal finished, the last step is submitting the form and handling it on the server. To implement this, create a new getToken() function in utils.sdk.js:

// ...
const getToken = async (amount) => {
  const PayNowSdk = PayNow.default;

  document.getElementById("card-token").value = "";
  document.getElementById("alert_message").innerHTML = "Verifying...";

  const cardToken = PayNowSdk().getCardToken();

  if (cardToken == null) {
    document.getElementById("alert_message").innerHTML =
      "Please verify the card information or use a different card.";
  } else {
    document.getElementById("card-token").value = cardToken;
    document.getElementById("alert_message").innerHTML = "Processing...";

    const formData = new FormData(document.getElementById("pay"));
    const avsFields = PayNowSdk().getAVSFields();

    return $fetch("/api/submit", {
      method: "POST",
      body: {
        cardToken,
        amount,
        ...Object.fromEntries(formData.entries()),
        ...avsFields,
      },
    });
  }

  return null;
};

export { initializeSDK, getToken };

The getToken() function uses the SDK to tokenize the card number and CVV inputs. This gives you a single token to work with while the most sensitive payment data is handled by North to ensure PCI compliance.

On top of that, the function uses the #alert_message element to inform the user about the progress. Finally, it retrieves the street and ZIP inputs using the .getAVSFields() method, and combines all the data to send a request to the server. For this, a useful $fetch() utility function is used that provides an isomorphic (i.e., working in both browser and Node.js environments) Fetch API–like interface for making HTTP requests.

To create the API route (/api/submit), create a new file called server/api/submit.js with the following code:

export default defineEventHandler(async (event) => {
  const config = useRuntimeConfig();
  const data = await readBody(event);
  const authBody = JSON.stringify({
    mid: config.public.mid,
    developerKey: config.developerKey,
    password: config.password,
  });
  const submitPaymentBody = JSON.stringify({
    token: data.cardToken,
    amount: data.amount,
    gateway_public_key: config.public.gatewayPublicKey,
    transaction_source: "PA-JS-SDK",
  });
  const authResponse = await $fetch("https://proxy.payanywhere.com/auth", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Content-Length": `${body.length}`,
      "x-nabwss-appsource": config.xNabwssAppsource,
    },
    body: authBody,
  });

  return $fetch(
    `https://proxy.payanywhere.com/mids/${config.public.mid}/gateways/payment`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Content-Length": `${submitPaymentBody.length}`,
        Authorization: `Bearer ${authResponse.token}`,
        "x-nabwss-appsource": config.xNabwssAppsource,
      },
      body: submitPaymentBody,
    }
  );
});

The route makes two requests using the data from the request body and credentials from the config:

  1. Authentication authenticates you with the API and provides a JWT token that's required to access the next endpoint.

  2. Payment submission submits the payment for processing using all the details given, including the transaction's amount, and the previously generated card token.

The end response is a JSON object of the following shape:

{
  "uniq_id": "ccs_329574",
  "response_code": "APR",
  "status_code": "00",
  "status_message": "00 - Approval",
  "last4": "1111",
  "exp_date": "2027-11-01",
  "network": "Mastercard",
  "cc_type": "Keyed",
  "sold_by": "account owner's email address",
  "is_duplicate": false,
  "requested_amt": "1.26",
  "authorized_amt": "1.26"
}

Returning the object from the route makes it into a JSON response.

To use the getToken() callback function and the new API route, go back to components/payment-modal.vue and add proper handling to the submitPayment() function:

HTML
<script setup>
  const config = useRuntimeConfig();
  const props = defineProps({
    opened: Boolean,
    amount: Number,
  });
  const emit = defineEmits(["closeModal"]);
  const submitPayment = async () => {
    const response = await getToken(props.amount);

    if (response?.response_code === "APR") {
      emit("closeModal");
    }
  };

  onMounted(() => {
    initializeSDK(config.public.mid, config.public.gatewayPublicKey);
  });
</script>

With that, the integration is ready.

How To Get Started

This article showed you how to integrate with North to embed payments into your Vue.js application. Thanks to the flexibility of the iFrame JS SDK and the modern features of JavaScript frameworks like Nuxt and Vue, the process is quick and relatively easy. Contact North's Sales Engineering team to learn more about how the iFrame JS SDK and other tools can meet your business needs.


Start your free Developer account and try it now.


©2025 North is a registered DBA of NorthAB, LLC. All rights reserved. North is a registered ISO of BMO Harris Bank N.A., Chicago, IL, Citizens Bank N.A., Providence, RI, The Bancorp Bank, Philadelphia, PA, FFB Bank, Fresno, CA, Wells Fargo Bank, N.A., Concord, CA, and PNC Bank, N.A.