Skip to main content

Overview

PIN operations use a secure token-based flow that keeps sensitive data completely isolated from your application. Card details and PIN viewing are delivered as secure images, while PIN setting uses hosted pages. You never handle or store sensitive PIN data.
Never attempt to handle, store, or transmit PIN data directly in your application. Always use the token-based flow.

Security Architecture

Token-Based Flow

Sensitive operations follow a simple pattern:
1

Generate Token

Request a single-use, time-limited token from the API
2

Display Secure Content

For viewing: Display secure image. For setting: Use PCI-compliant hosted page
3

Token Expires

Token is invalidated after use or timeout (~10 minutes)

Benefits

PCI Compliance

No sensitive data touches your infrastructure

Zero Liability

Reduces security audit scope

Built-in Security

Session management and expiration

User Trust

Industry-standard secure pages

Viewing Card Details

Display full card information (PAN, CVV, expiry) as a secure image. The card details are never transmitted to or stored by your application.

Step 1: Generate Details Token

const response = await fetch('https://dev.api.baanx.com/v1/card/details/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Client-ID': 'your_client_id',
    'Authorization': `Bearer ${userAccessToken}`
  },
  body: JSON.stringify({
    customCss: {
      cardBackgroundColor: '#000000',
      cardTextColor: '#FFFFFF',
      panBackgroundColor: '#EFEFEF',
      panTextColor: '#000000'
    }
  })
});

const { token, imageUrl } = await response.json();

Request Parameters

customCss
object
Optional styling to match your brand. If omitted, default styling is applied.

Response

{
  "token": "100a99cf-f4d3-4fa1-9be9-2e9828b20ebb",
  "imageUrl": "https://cards.baanx.com/details-image?token=100a99cf-f4d3-4fa1-9be9-2e9828b20ebb"
}
token
string
Single-use token valid for ~10 minutes
imageUrl
string
URL that renders card details as a secure image. Use as src attribute of an <img> tag.
Security: Treat imageUrl as highly sensitive. Never log or store it. Use HTTPS only and display in secure contexts.

Step 2: Display Card Image

function showCardDetails(imageUrl) {
  const img = document.createElement('img');
  img.src = imageUrl;
  img.alt = 'Card Details';
  img.style.cssText = `
    max-width: 100%;
    border-radius: 12px;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  `;

  document.getElementById('card-details-container').appendChild(img);
}

Complete React Implementation

import { useState } from 'react';

export function CardDetailsViewer({ clientId, accessToken }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [imageUrl, setImageUrl] = useState(null);

  async function viewCardDetails() {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('https://dev.api.baanx.com/v1/card/details/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Client-ID': clientId,
          'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify({
          customCss: {
            cardBackgroundColor: '#000000',
            cardTextColor: '#FFFFFF',
            panBackgroundColor: '#EFEFEF',
            panTextColor: '#000000'
          }
        })
      });

      if (!response.ok) {
        throw new Error('Failed to generate details token');
      }

      const data = await response.json();
      setImageUrl(data.imageUrl);

    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  function hideCardDetails() {
    setImageUrl(null);
  }

  if (imageUrl) {
    return (
      <div className="card-details-viewer">
        <div className="overlay" onClick={hideCardDetails} />
        <div className="modal">
          <button className="close-btn" onClick={hideCardDetails}>

          </button>
          <img
            src={imageUrl}
            alt="Card Details"
            style={{
              maxWidth: '100%',
              borderRadius: '12px'
            }}
          />
        </div>
      </div>
    );
  }

  return (
    <div>
      <button
        onClick={viewCardDetails}
        disabled={loading}
        className="btn-view-details"
      >
        {loading ? 'Loading...' : '👁️ View Card Details'}
      </button>

      {error && (
        <div className="error-message">{error}</div>
      )}
    </div>
  );
}

Viewing PIN

Display the card PIN as a secure image. The PIN is never transmitted to or stored by your application.

Step 1: Generate PIN Token

const response = await fetch('https://dev.api.baanx.com/v1/card/pin/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Client-ID': 'your_client_id',
    'Authorization': `Bearer ${userAccessToken}`
  },
  body: JSON.stringify({
    customCss: {
      backgroundColor: '#EFEFEF',
      textColor: '#000000'
    }
  })
});

const { token, imageUrl } = await response.json();

Request Parameters

customCss
object
Optional styling to match your brand. If omitted, default styling is applied.

Response

{
  "token": "100a99cf-f4d3-4fa1-9be9-2e9828b20ebb",
  "imageUrl": "https://cards.baanx.com/details-image?token=100a99cf-f4d3-4fa1-9be9-2e9828b20ebb"
}
token
string
Single-use token valid for ~10 minutes
imageUrl
string
URL that renders PIN as a secure image. Use as src attribute of an <img> tag.
Security: Treat imageUrl as highly sensitive. Never log or store it. Display only in authenticated, secure contexts.

Step 2: Display PIN Image

function showPIN(imageUrl) {
  const img = document.createElement('img');
  img.src = imageUrl;
  img.alt = 'Card PIN';
  img.style.cssText = `
    max-width: 100%;
    border-radius: 8px;
  `;

  document.getElementById('pin-container').appendChild(img);
}

Complete React Implementation

import { useState } from 'react';

export function PINViewer({ clientId, accessToken }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [imageUrl, setImageUrl] = useState(null);

  async function viewPIN() {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('https://dev.api.baanx.com/v1/card/pin/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Client-ID': clientId,
          'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify({
          customCss: {
            backgroundColor: '#EFEFEF',
            textColor: '#000000'
          }
        })
      });

      if (!response.ok) {
        throw new Error('Failed to generate PIN token');
      }

      const data = await response.json();
      setImageUrl(data.imageUrl);

    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  function hidePIN() {
    setImageUrl(null);
  }

  if (imageUrl) {
    return (
      <div className="pin-viewer">
        <div className="overlay" onClick={hidePIN} />
        <div className="modal">
          <button className="close-btn" onClick={hidePIN}>

          </button>
          <img
            src={imageUrl}
            alt="Card PIN"
            style={{
              maxWidth: '100%',
              borderRadius: '8px'
            }}
          />
        </div>
      </div>
    );
  }

  return (
    <div>
      <button
        onClick={viewPIN}
        disabled={loading}
        className="btn-view-pin"
      >
        {loading ? 'Loading...' : '👁️ View PIN'}
      </button>

      {error && (
        <div className="error-message">{error}</div>
      )}
    </div>
  );
}

Setting/Changing PIN

Allow users to create or change their PIN through a secure hosted page.

Step 1: Generate Set PIN Token

const response = await fetch('https://dev.api.baanx.com/v1/card/set-pin/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-Client-ID': 'your_client_id',
    'Authorization': `Bearer ${userAccessToken}`
  },
  body: JSON.stringify({
    isEmbedded: true,
    customCss: {
      backgroundColor: '#EFEFEF',
      textColor: '#000000',
      backgroundColorPrimary: '#000000',
      textColorPrimary: '#FFFFFF',
      buttonBorderRadius: 8,
      pinBorderRadius: 4
    }
  })
});

const { token, hostedPageUrl } = await response.json();

Request Parameters

Same as PIN viewing, with identical customCss options.

Response

{
  "token": "100a99cf-f4d3-4fa1-9be9-2e9828b20ebb",
  "hostedPageUrl": "https://cards.baanx.com/pin-direct/set?token=100a99cf-f4d3-4fa1-9be9-2e9828b20ebb"
}
Set PIN hosted page includes built-in validation for PIN requirements (4 digits, no sequential patterns, etc.)

Step 2: Display Set PIN Page

function showSetPIN(hostedPageUrl) {
  const iframe = document.createElement('iframe');
  iframe.src = hostedPageUrl;
  iframe.style.cssText = `
    width: 100%;
    height: 450px;
    border: none;
    border-radius: 8px;
  `;

  document.getElementById('pin-container').appendChild(iframe);

  window.addEventListener('message', (event) => {
    if (event.origin !== 'https://cards.baanx.com') return;

    switch (event.data) {
      case 'success':
        console.log('PIN set successfully');
        iframe.remove();
        showSuccessMessage('PIN updated successfully');
        break;
      case 'abort':
        console.log('User cancelled PIN change');
        iframe.remove();
        break;
      case 'error':
        console.error('Error setting PIN');
        iframe.remove();
        showErrorMessage('Failed to set PIN');
        break;
    }
  });
}

Complete PIN Management Component

import { useState } from 'react';

export function PINManagement({ clientId, accessToken, cardStatus }) {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [activeView, setActiveView] = useState(null);
  const [imageUrl, setImageUrl] = useState(null);
  const [hostedPageUrl, setHostedPageUrl] = useState(null);

  async function generateToken(endpoint) {
    setLoading(true);
    setError(null);

    try {
      const customCss = endpoint === 'pin'
        ? {
            backgroundColor: '#f5f5f5',
            textColor: '#1a1a1a'
          }
        : {
            backgroundColor: '#f5f5f5',
            textColor: '#1a1a1a',
            backgroundColorPrimary: '#2563eb',
            textColorPrimary: '#ffffff',
            buttonBorderRadius: 8,
            pinBorderRadius: 4
          };

      const requestBody = endpoint === 'pin'
        ? { customCss }
        : { isEmbedded: true, customCss };

      const response = await fetch(`https://dev.api.baanx.com/v1/card/${endpoint}/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Client-ID': clientId,
          'Authorization': `Bearer ${accessToken}`
        },
        body: JSON.stringify(requestBody)
      });

      if (!response.ok) {
        throw new Error('Failed to generate token');
      }

      const data = await response.json();

      if (endpoint === 'pin') {
        setImageUrl(data.imageUrl);
      } else {
        setHostedPageUrl(data.hostedPageUrl);
        window.addEventListener('message', handleMessage);
      }

      setActiveView(endpoint);

    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  function handleMessage(event) {
    if (event.origin !== 'https://cards.baanx.com') return;

    switch (event.data) {
      case 'success':
        closeView();
        break;
      case 'abort':
        closeView();
        break;
      case 'error':
        setError('Operation failed');
        closeView();
        break;
    }
  }

  function closeView() {
    setActiveView(null);
    setImageUrl(null);
    setHostedPageUrl(null);
    window.removeEventListener('message', handleMessage);
  }

  if (activeView === 'pin' && imageUrl) {
    return (
      <div className="pin-modal">
        <div className="overlay" onClick={closeView} />
        <div className="modal-content">
          <button className="close-btn" onClick={closeView}></button>
          <img
            src={imageUrl}
            alt="Card PIN"
            style={{
              maxWidth: '100%',
              borderRadius: '8px'
            }}
          />
        </div>
      </div>
    );
  }

  if (activeView === 'set-pin' && hostedPageUrl) {
    return (
      <div className="pin-modal">
        <div className="overlay" onClick={closeView} />
        <div className="modal-content">
          <button className="close-btn" onClick={closeView}></button>
          <iframe
            src={hostedPageUrl}
            style={{
              width: '100%',
              height: '450px',
              border: 'none',
              borderRadius: '8px'
            }}
          />
        </div>
      </div>
    );
  }

  const isFrozen = cardStatus === 'FROZEN';
  const isBlocked = cardStatus === 'BLOCKED';

  return (
    <div className="pin-management">
      <h3>PIN Management</h3>

      {error && <div className="error-message">{error}</div>}

      <div className="pin-actions">
        <button
          onClick={() => generateToken('pin')}
          disabled={loading || isBlocked}
          className="btn-view-pin"
        >
          {loading ? 'Loading...' : '👁️ View PIN'}
        </button>

        <button
          onClick={() => generateToken('set-pin')}
          disabled={loading || isFrozen || isBlocked}
          className="btn-set-pin"
        >
          {loading ? 'Loading...' : '🔐 Change PIN'}
        </button>
      </div>

      {isFrozen && (
        <p className="info-text">
          Unfreeze your card to change PIN
        </p>
      )}

      {isBlocked && (
        <p className="error-text">
          Card is blocked - contact support
        </p>
      )}
    </div>
  );
}

Sequence Diagrams

View Card Details Flow

View PIN Flow

Set PIN Flow

Customization Examples

Dark Theme

// Card details viewing
const darkThemeCardDetails = {
  cardBackgroundColor: '#1a1a1a',
  cardTextColor: '#ffffff',
  panBackgroundColor: '#2a2a2a',
  panTextColor: '#ffffff'
};

// PIN viewing
const darkThemePIN = {
  backgroundColor: '#1a1a1a',
  textColor: '#ffffff'
};

// PIN setting (hosted page)
const darkThemeSetPIN = {
  backgroundColor: '#1a1a1a',
  textColor: '#ffffff',
  backgroundColorPrimary: '#3b82f6',
  textColorPrimary: '#ffffff',
  buttonBorderRadius: 8,
  pinBorderRadius: 4
};

Light Theme

// Card details viewing
const lightThemeCardDetails = {
  cardBackgroundColor: '#ffffff',
  cardTextColor: '#1a1a1a',
  panBackgroundColor: '#f5f5f5',
  panTextColor: '#1a1a1a'
};

// PIN viewing
const lightThemePIN = {
  backgroundColor: '#ffffff',
  textColor: '#1a1a1a'
};

// PIN setting (hosted page)
const lightThemeSetPIN = {
  backgroundColor: '#ffffff',
  textColor: '#1a1a1a',
  backgroundColorPrimary: '#2563eb',
  textColorPrimary: '#ffffff',
  buttonBorderRadius: 8,
  pinBorderRadius: 4
};

Brand Theme

// Card details viewing
const brandThemeCardDetails = {
  cardBackgroundColor: '#7c3aed', // Brand purple
  cardTextColor: '#ffffff',
  panBackgroundColor: '#f5f5f5',
  panTextColor: '#1a1a1a'
};

// PIN viewing
const brandThemePIN = {
  backgroundColor: '#fafafa',
  textColor: '#7c3aed'
};

// PIN setting (hosted page)
const brandThemeSetPIN = {
  backgroundColor: '#fafafa',
  textColor: '#1a1a1a',
  backgroundColorPrimary: '#7c3aed', // Brand purple
  textColorPrimary: '#ffffff',
  buttonBorderRadius: 12,
  pinBorderRadius: 8
};

Security Best Practices

Token Handling

Never store tokens in localStorage or sessionStorage
Use tokens immediately after generation
Don’t reuse tokens - generate new ones for each operation
Implement token expiration handling

Image URL Security

Image URLs contain sensitive data. Never log, cache, or store them. Use HTTPS only and display only in authenticated contexts.
// ✅ Good: Use imageUrl immediately and clear after viewing
const { imageUrl } = await response.json();
displayImage(imageUrl);

// Clean up when user closes
onClose(() => {
  removeImageFromDOM();
  // imageUrl is garbage collected
});

// ❌ Bad: Don't store or log
localStorage.setItem('pinImage', imageUrl); // NEVER
console.log('Image URL:', imageUrl); // NEVER

iframe Security (Set PIN Only)

For PIN setting operations that use hosted pages:
<iframe
  src="{hostedPageUrl}"
  sandbox="allow-scripts allow-same-origin allow-forms"
  allow="clipboard-write"
  referrerpolicy="no-referrer"
  style="border: none;"
/>

Origin Verification (Set PIN Only)

Only needed for PIN setting hosted pages:
window.addEventListener('message', (event) => {
  // Always verify origin
  if (event.origin !== 'https://cards.baanx.com') {
    console.warn('Rejected message from unauthorized origin:', event.origin);
    return;
  }

  // Process message
  handleMessage(event.data);
});

Content Security Policy

<meta http-equiv="Content-Security-Policy" content="
  frame-src https://cards.baanx.com;
  img-src https://cards.baanx.com;
  script-src 'self' 'unsafe-inline';
  connect-src https://dev.api.baanx.com;
">

Troubleshooting

Cause: Token has ~10 minute lifetimeSolution: Generate token only when user clicks action button
// ❌ Bad: Generate token on page load
useEffect(() => {
  generateToken(); // May expire before use
}, []);

// ✅ Good: Generate on user action
<button onClick={generateToken}>View PIN</button>
Cause: Token expired, already used, or network issueSolution: Implement error handling and retry logic
const img = document.createElement('img');

img.onerror = () => {
  console.error('Failed to load image');
  alert('Unable to display. Please try again.');
};

img.onload = () => {
  console.log('Image loaded successfully');
};

img.src = imageUrl;
Cause: Color contrast issues or invalid hex codesSolution: Ensure valid hex codes and sufficient contrast
// ❌ Bad: Same colors for background and text
{
  backgroundColor: '#000000',
  textColor: '#000000' // Can't read!
}

// ✅ Good: High contrast
{
  backgroundColor: '#000000',
  textColor: '#ffffff'
}
Cause: Origin verification failing or listener not attachedSolution: Verify origin and ensure listener is active (only applies to set-PIN hosted page)
window.addEventListener('message', (event) => {
  console.log('Received message:', event.data, 'from:', event.origin);

  if (event.origin !== 'https://cards.baanx.com') {
    console.warn('Wrong origin');
    return;
  }

  handleMessage(event.data);
});
Cause: PIN changes not allowed on frozen cardsSolution: Check card status and disable PIN change button
<button
  onClick={changePIN}
  disabled={cardStatus === 'FROZEN' || cardStatus === 'BLOCKED'}
>
  Change PIN
</button>

Next Steps