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:
- Hosted card-entry page — recommended for all applications.
- 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.
Your Mbanq access token must remain on a trusted backend. Never expose it in browser or mobile application code.
How the integration works
- Your browser requests card-entry configuration from your backend.
- Your backend retrieves the configuration from Mbanq.
- The browser uses the returned configuration:
- If
iframeUrlis present, it loads the hosted card-entry page. - Otherwise, it loads the TabaPay SDK using
sdkUrlandbrowserClientId.
- If
- The customer enters their card information in the provider-hosted form.
- TabaPay returns a short-lived encrypted card token.
- The browser sends only the token to your backend.
- Your backend immediately submits the token to Mbanq.
- 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:
| Value | Description |
|---|---|
https://api.dev.mbanq.cloud | Mbanq 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
externalcardsresource 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
| Field | Required | Description |
|---|---|---|
iframeUrl | No | URL of the hosted card-entry page |
sdkUrl | No | Public URL from which a direct integration loads the SDK |
browserClientId | No | Public identifier used to initialize the Browser SDK |
A valid response contains either:
iframeUrlor- Both
sdkUrlandbrowserClientId.
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/configurationExample 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.cloudIf 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-tokenOnly 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:
- Authenticate the current application user.
- Confirm that the user may add a card to the requested Mbanq client.
- Read
cardTokenfrom the request body. - Submit it immediately to Mbanq.
- Return only safe result fields.
- 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:
| Field | Required | Description |
|---|---|---|
cardToken | Yes | Short-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.resourceIdis present.
Do not log GraphQL variables containing cardToken.
7. Handle the result
REST and GraphQL return the same relevant fields:
| Field | Description |
|---|---|
clientId | ID of the client who owns the Acquired Card |
resourceId | ID 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
| Scenario | Recommended action |
|---|---|
| Configuration cannot be loaded | Do not render the form; ask the customer to try again later |
| Hosted iframe cannot be loaded | Display a generic card-entry unavailable message |
| Parent origin does not match | Correct the hosted-page configuration; do not use a wildcard origin |
| Browser SDK cannot be loaded | Display a generic card-entry unavailable message |
| SDK initialization fails | Remove the incomplete form and display a generic error |
| Tokenization fails | Render a new provider form |
| Token expires before submission | Render a new form and obtain a new token |
| Token was already submitted | Obtain a new token |
| Client is not eligible | Display an appropriate message without generating another token automatically |
| Card limit is reached | Do not resubmit the token |
| Card is duplicate or blocked | Do not repeatedly submit the token |
| GraphQL returns errors | Treat the operation as unsuccessful |
| Submission result is uncertain | Do 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
iframeUrlwhen it is returned. - Load the SDK only from the configured trusted URL.
- Collect card information only through the provider-hosted form.
- Validate both
event.originandevent.sourcefor iframe messages. - Configure an exact parent origin for hosted web integrations.
- Never use
*as the productionpostMessagetarget 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 bothsdkUrlandbrowserClientId. - 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.
Updated about 17 hours ago

