Tutorial: Add an Acquired Card

This tutorial explains how to securely add an Acquired Card using the Mbanq Cloud API.

Mbanq supports two browser integration methods:

  1. Hosted card-entry page — recommended for all applications.
  2. Direct TabaPay Browser SDK — available when Mbanq returns the SDK configuration.

Both methods collect card information through TabaPay and return a short-lived encrypted token. Your application submits only that token to Mbanq.

📘

API reference

Your Mbanq access token must remain on a trusted backend. Never expose it in browser or mobile application code.

How the integration works

  1. Your browser requests card-entry configuration from your backend.
  2. Your backend retrieves the configuration from Mbanq.
  3. The browser uses the returned configuration:
    • If iframeUrl is present, it loads the hosted card-entry page.
    • Otherwise, it loads the TabaPay SDK using sdkUrl and browserClientId.
  4. The customer enters their card information in the provider-hosted form.
  5. TabaPay returns a short-lived encrypted card token.
  6. The browser sends only the token to your backend.
  7. Your backend immediately submits the token to Mbanq.
  8. Mbanq creates the Acquired Card and returns its resource ID.

Prerequisites

Before you begin, you need:

  • An Mbanq Cloud API account
  • A valid Mbanq API access token
  • The ID of the client receiving the Acquired Card
  • A browser application served over HTTPS
  • A trusted backend that can securely store the Mbanq access token

Replace these example values with your own:

ValueDescription
https://api.dev.mbanq.cloudMbanq Cloud API base URL
<access-token>Mbanq access token stored securely by your backend
<client-id>ID of the client receiving the Acquired Card
<tenant-id>Mbanq tenant identifier
<device-fingerprint>Unique identifier associated with the client device

API paths use the existing externalcards resource name. In user-facing text, this tutorial refers to the resource as an Acquired Card.

1. Retrieve card-entry configuration

Your backend must retrieve the card-entry configuration from Mbanq.

Do not call this endpoint from publicly distributed browser code if doing so requires exposing your Mbanq access token.

Request

curl --request GET \
  --url "https://api.dev.mbanq.cloud/v1/externalcards/configuration" \
  --header "Authorization: Bearer <access-token>" \
  --header "Accept: application/json"

Hosted iframe response

When a hosted card-entry page is available, the response can contain:

{
  "sdkUrl": null,
  "browserClientId": null,
  "iframeUrl": "https://card-entry.dev.mbanq.cloud"
}

When iframeUrl is present, sdkUrl and browserClientId are optional.

Direct SDK response

When the consumer must load the Browser SDK directly, the response can contain:

{
  "sdkUrl": "https://iframes.sandbox.tabapay.net/TabaPaySDK.js",
  "browserClientId": "<public-browser-client-id>",
  "iframeUrl": null
}

When iframeUrl is absent, both sdkUrl and browserClientId are required.

Response fields

FieldRequiredDescription
iframeUrlNoURL of the hosted card-entry page
sdkUrlNoPublic URL from which a direct integration loads the SDK
browserClientIdNoPublic identifier used to initialize the Browser SDK

A valid response contains either:

  • iframeUrl or
  • Both sdkUrl and browserClientId.

The values may differ between sandbox and production. Do not hard-code them in application source code.

The iframe URL, SDK URL, and Browser Client ID are browser-visible configuration. They are not private API credentials.

2. Expose configuration through your backend

Create an endpoint in your application that retrieves the configuration from Mbanq and returns the card-entry fields to your browser.

For example:

GET /api/card-acquiring/configuration

Example backend response:

{
  "sdkUrl": null,
  "browserClientId": null,
  "iframeUrl": "https://card-entry.dev.mbanq.cloud"
}

The following browser helper retrieves the configuration:

async function getCardEntryConfiguration() {
  const response = await fetch("/api/card-acquiring/configuration", {
    method: "GET",
    headers: {
      Accept: "application/json"
    },
    credentials: "same-origin"
  });

  if (!response.ok) {
    throw new Error("Card entry is currently unavailable.");
  }

  const configuration = await response.json();

  const hasIframe = Boolean(configuration.iframeUrl);
  const hasDirectSdk = Boolean(
    configuration.sdkUrl && configuration.browserClientId
  );

  if (!hasIframe && !hasDirectSdk) {
    throw new Error("Unsupported card-entry configuration.");
  }

  return configuration;
}

3. Use the hosted card-entry page

Use this method when iframeUrl is present.

Add the page elements

<div id="card-entry-container">
  <p id="card-entry-status" role="status">
    Loading secure card entry...
  </p>
</div>

Load the iframe

let cardEntryFrame;
let cardEntryOrigin;
let submitting = false;

async function initializeHostedCardEntry({ clientId }) {
  const container = document.querySelector("#card-entry-container");
  const status = document.querySelector("#card-entry-status");

  if (!container || !status) {
    throw new Error("The card-entry container is missing.");
  }

  const configuration = await getCardEntryConfiguration();

  if (!configuration.iframeUrl) {
    throw new Error("Hosted card entry is not configured.");
  }

  const iframeUrl = new URL(
    configuration.iframeUrl,
    window.location.origin
  );

  if (iframeUrl.protocol !== "https:") {
    throw new Error("The card-entry URL must use HTTPS.");
  }

  cardEntryOrigin = iframeUrl.origin;

  cardEntryFrame = document.createElement("iframe");
  cardEntryFrame.src = iframeUrl.href;
  cardEntryFrame.title = "Add an acquired card";
  cardEntryFrame.style.width = "100%";
  cardEntryFrame.style.minHeight = "560px";
  cardEntryFrame.style.border = "0";

  window.addEventListener("message", event => {
    handleCardEntryMessage({
      event,
      clientId,
      status
    });
  });

  container.appendChild(cardEntryFrame);
}

Handle messages from the iframe

The hosted page sends messages using window.postMessage().

Supported messages are:

{
  "type": "ready"
}
{
  "type": "submit",
  "payload": "<encrypted-card-token>"
}
{
  "type": "cancel"
}
{
  "type": "error",
  "payload": "<safe-error-code>"
}

Validate both event.origin and event.source. Never accept messages based only on their contents.

async function handleCardEntryMessage({
  event,
  clientId,
  status
}) {
  if (
    !cardEntryFrame ||
    event.origin !== cardEntryOrigin ||
    event.source !== cardEntryFrame.contentWindow
  ) {
    return;
  }

  const message = event.data;

  if (!message || typeof message.type !== "string") {
    return;
  }

  switch (message.type) {
    case "ready":
      status.textContent = "";
      break;

    case "submit":
      await submitHostedCardToken({
        clientId,
        cardToken: message.payload,
        status
      });
      break;

    case "cancel":
      status.textContent = "Card entry was canceled.";
      break;

    case "error":
      status.textContent = "Card entry is currently unavailable.";
      break;
  }
}

Submit the hosted token

The hosted page returns only the encrypted token in the submit message.

async function submitHostedCardToken({
  clientId,
  cardToken,
  status
}) {
  if (submitting) {
    return;
  }

  if (typeof cardToken !== "string" || !cardToken) {
    status.textContent =
      "The card-entry service returned an invalid token.";
    return;
  }

  submitting = true;
  status.textContent = "Adding card...";

  try {
    const result = await addAcquiredCard({
      clientId,
      cardToken
    });

    status.textContent =
      `Card added successfully. Resource ID: ${result.resourceId}`;
  } catch {
    status.textContent =
      "The card could not be added. Please enter the card again.";

    reloadHostedCardEntry();
  } finally {
    submitting = false;
  }
}

function reloadHostedCardEntry() {
  if (!cardEntryFrame) {
    return;
  }

  const iframeUrl = cardEntryFrame.src;
  cardEntryFrame.src = "about:blank";
  cardEntryFrame.src = iframeUrl;
}

Initialize the page using the Mbanq client ID:

initializeHostedCardEntry({
  clientId: 123
}).catch(() => {
  document.querySelector("#card-entry-status").textContent =
    "Card entry is currently unavailable.";
});

Parent-origin requirement

The hosted card-entry deployment must allow the exact origin of the parent web application.

For example:

https://mobile-middleware.dev.mbanq.cloud

If the configured parent origin does not match, the iframe may appear normally, but ready, submit, cancel, and error messages will not reach the parent application.

Never use * as the production postMessage target origin.

4. Use the Browser SDK directly

Use this method only when iframeUrl is absent and both sdkUrl and browserClientId are present.

Add the SDK container

<div id="card-entry"></div>
<p id="card-entry-message" role="status"></p>

Load the SDK

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const existingScript = Array.from(
      document.querySelectorAll("script[data-card-entry-sdk]")
    ).find(script => script.src === src);

    if (existingScript) {
      if (window.tabaPaySdk?.createIframe) {
        resolve();
        return;
      }

      existingScript.addEventListener("load", resolve, {
        once: true
      });

      existingScript.addEventListener(
        "error",
        () => reject(
          new Error("Unable to load the card-entry SDK.")
        ),
        { once: true }
      );

      return;
    }

    const script = document.createElement("script");

    script.src = src;
    script.async = true;
    script.referrerPolicy = "no-referrer";
    script.dataset.cardEntrySdk = "true";

    script.onload = () => {
      if (!window.tabaPaySdk?.createIframe) {
        reject(
          new Error("The card-entry SDK did not initialize.")
        );
        return;
      }

      resolve();
    };

    script.onerror = () => {
      reject(
        new Error("Unable to load the card-entry SDK.")
      );
    };

    document.head.appendChild(script);
  });
}

Only load the SDK URL returned through your trusted backend.

Extract the token

TabaPay returns a value containing three pipe-delimited fields:

card-last4|expiration-date|encrypted-card-token

Only the third field must be submitted to Mbanq as cardToken.

function extractCardToken(encryptedCardData) {
  if (typeof encryptedCardData !== "string") {
    throw new Error(
      "The card-entry SDK returned an invalid value."
    );
  }

  const fields = encryptedCardData.split("|");

  if (fields.length !== 3 || !fields[2]) {
    throw new Error(
      "The card-entry SDK returned an invalid token."
    );
  }

  return fields[2];
}

Do not log the complete SDK response, any of its fields, or the extracted token.

Render the SDK iframe

async function initializeDirectCardEntry({ clientId }) {
  const target = document.querySelector("#card-entry");
  const message = document.querySelector(
    "#card-entry-message"
  );

  if (!target || !message) {
    throw new Error("The card-entry container is missing.");
  }

  const configuration = await getCardEntryConfiguration();

  if (configuration.iframeUrl) {
    throw new Error(
      "Use the hosted iframe integration for this configuration."
    );
  }

  if (
    !configuration.sdkUrl ||
    !configuration.browserClientId
  ) {
    throw new Error(
      "Direct Browser SDK configuration is incomplete."
    );
  }

  await loadScript(configuration.sdkUrl);

  const { element } = window.tabaPaySdk.createIframe({
    clientId: configuration.browserClientId,

    cardNumberInput: {
      separator: " "
    },

    expirationDateInput: {
      placeholderText: "MM/YY"
    },

    cscInput: {
      labelText: "CVV"
    },

    buttons: {
      submit: {
        order: "1"
      },
      reset: {
        order: "2"
      },
      cancel: {
        order: "3"
      }
    },

    eventListeners: {
      submit: async encryptedCardData => {
        message.textContent = "Adding card...";

        try {
          const cardToken =
            extractCardToken(encryptedCardData);

          const result = await addAcquiredCard({
            clientId,
            cardToken
          });

          message.textContent =
            `Card added successfully. Resource ID: ${result.resourceId}`;
        } catch {
          message.textContent =
            "The card could not be added. Please enter the card again.";
        }
      },

      cancel: () => {
        message.textContent =
          "Card entry was canceled.";
      }
    }
  });

  target.replaceChildren(element);
}

Initialize the direct SDK form:

initializeDirectCardEntry({
  clientId: 123
}).catch(() => {
  document.querySelector("#card-entry-message").textContent =
    "Card entry is currently unavailable.";
});

For additional customization options, see the TabaPay Browser SDK reference.

5. Send the token to your backend

The browser must send the token to your trusted backend, not directly to Mbanq.

async function addAcquiredCard({
  clientId,
  cardToken
}) {
  const response = await fetch(
    `/api/clients/${encodeURIComponent(clientId)}/acquired-cards`,
    {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      credentials: "same-origin",
      body: JSON.stringify({
        cardToken
      })
    }
  );

  const result = await response.json().catch(() => null);

  if (!response.ok) {
    throw new Error("Unable to add the card.");
  }

  return result;
}

The /api/clients/{clientId}/acquired-cards path in this example belongs to your application. It is not an Mbanq endpoint.

Your backend should:

  1. Authenticate the current application user.
  2. Confirm that the user may add a card to the requested Mbanq client.
  3. ReadcardTokenfrom the request body.
  4. Submit it immediately to Mbanq.
  5. Return only safe result fields.
  6. Discard the token after the request.

6. Add the Acquired Card through Mbanq

Your backend can use either REST or GraphQL.

Choose one API style. Do not submit the same token through both APIs.

Option A: REST

curl --request POST \
  --url "https://api.cloud.mbanq.com/v1/clients/<client-id>/externalcards" \
  --header "Authorization: Bearer <access-token>" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json" \
  --data '{
    "cardToken": "<browser-sdk-token>"
  }'

The request body contains:

FieldRequiredDescription
cardTokenYesShort-lived encrypted token from card entry

A successful response contains:

{
  "clientId": 123,
  "resourceId": 456
}

Option B: GraphQL

Use createExternalCard when your backend communicates with Mbanq through GraphQL.

mutation AddAcquiredCard(
  $clientId: Long!
  $cardToken: String!
) {
  createExternalCard(
    input: {
      clientId: $clientId
      cardToken: $cardToken
    }
  ) {
    clientId
    resourceId
  }
}

Send the query and variables from your backend:

curl --request POST \
  --url "https://api.cloud.mbanq.com/graphql" \
  --header "Authorization: Bearer <access-token>" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json" \
  --data '{
    "query": "mutation AddAcquiredCard($clientId: Long!, $cardToken: String!) { createExternalCard(input: { clientId: $clientId, cardToken: $cardToken }) { clientId resourceId } }",
    "variables": {
      "clientId": 123,
      "cardToken": "<browser-sdk-token>"
    }
  }'

A successful GraphQL response contains:

{
  "data": {
    "createExternalCard": {
      "clientId": 123,
      "resourceId": 456
    }
  }
}

GraphQL can return an errors array even when the HTTP request succeeds. Treat the operation as successful only when:

  • The HTTP response indicates success.
  • The response does not contain GraphQL errors.
  • data.createExternalCard.resourceId is present.

Do not log GraphQL variables containing cardToken.

7. Handle the result

REST and GraphQL return the same relevant fields:

FieldDescription
clientIdID of the client who owns the Acquired Card
resourceIdID of the Acquired Card resource created by Mbanq

Store resourceId if your application needs to reference the Acquired Card later.

The response does not return the submitted cardToken.

Error and retry handling

ScenarioRecommended action
Configuration cannot be loadedDo not render the form; ask the customer to try again later
Hosted iframe cannot be loadedDisplay a generic card-entry unavailable message
Parent origin does not matchCorrect the hosted-page configuration; do not use a wildcard origin
Browser SDK cannot be loadedDisplay a generic card-entry unavailable message
SDK initialization failsRemove the incomplete form and display a generic error
Tokenization failsRender a new provider form
Token expires before submissionRender a new form and obtain a new token
Token was already submittedObtain a new token
Client is not eligibleDisplay an appropriate message without generating another token automatically
Card limit is reachedDo not resubmit the token
Card is duplicate or blockedDo not repeatedly submit the token
GraphQL returns errorsTreat the operation as unsuccessful
Submission result is uncertainDo not automatically retry the same token

The token is short-lived. Submit it immediately after receiving it.

Do not automatically retry a failed or uncertain request using the same token. When another attempt is appropriate, render a new provider form and obtain a new token.

Security requirements

Your integration must:

  • Serve production browser applications over HTTPS.
  • Keep the Mbanq access token on a trusted backend.
  • Retrieve card-entry configuration dynamically.
  • Prefer iframeUrl when it is returned.
  • Load the SDK only from the configured trusted URL.
  • Collect card information only through the provider-hosted form.
  • Validate both event.origin and event.source for iframe messages.
  • Configure an exact parent origin for hosted web integrations.
  • Never use * as the production postMessage target origin.
  • Extract and submit only the encrypted token.
  • Submit each token immediately and only once.
  • Never submit the same token through both REST and GraphQL.
  • Never include the token in a URL or query parameter.
  • Never store the token in browser storage or a database.
  • Never place the token in logs, traces, analytics, events, or queues.
  • Never log request bodies or GraphQL variables containing the token.
  • Remove sensitive values from errors before sending them to monitoring.
  • Authorize the application user before adding a card to the requested client.
  • Keep API keys, private keys, and provider credentials on trusted servers.

The SDK URL, iframe URL, and Browser Client ID cannot be hidden from browser users. Do not use them as authentication credentials.

Production checklist

Before enabling Card Acquiring in production, confirm that:

  • The backend uses the production Mbanq Cloud API.
  • Card-entry configuration is retrieved dynamically.
  • The response contains iframeUrl, or both sdkUrl and browserClientId.
  • The browser application and hosted iframe use HTTPS.
  • The production card-entry URL is configured.
  • The hosted page allows the exact production parent origin.
  • The Mbanq access token is available only to the backend.
  • Card data is collected only by the provider-hosted form.
  • Iframe messages are validated by origin and source.
  • Only the encrypted token is submitted to Mbanq.
  • Tokens are submitted immediately and only once.
  • Sensitive values are excluded from monitoring and logs.
  • Failed or uncertain requests are not automatically retried.
  • Error messages do not expose sensitive values.
  • Sandbox configuration and test data have been removed.
📘

Sandbox Test Cards


Did this page help you?