Skip to main content
Card elements let you embed secure card input fields in your own page. Each field runs in a cross-origin iframe — raw card data never exists in your JavaScript or reaches your server.

Field Types

typeDescription
cardNumberFull card number (13–19 digits) with Luhn validation, brand detection, and formatting (e.g. xxxx xxxx xxxx xxxx for 16-digit cards)
expirationDateMM / YY expiry with future-date validation
cvv3-digit CVV (or 4-digit for Amex, auto-adjusts based on detected brand)

Creating and Mounting

OzVault.create() is the async static factory — call it once when your checkout loads. It resolves once the wax key is obtained; mount elements immediately after. The vault is ready to tokenize once onReady fires (or vault.isReady === true). Gate your submit button on both vault readiness and field readiness.
import { OzVault, OzError, createFetchWaxKey } from '@ozura/elements';

let vault;
try {
  vault = await OzVault.create({
    pubKey:      'YOUR_PUB_KEY',
    fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
  });
} catch (err) {
  // Pub key blocked, fetchWaxKey failed, or backend unreachable
  console.error('Vault init failed:', err instanceof OzError ? err.message : err);
  // Show fallback UI — do not proceed
  return;
}

const cardNumber = vault.createElement('cardNumber');
const expiry     = vault.createElement('expirationDate');
const cvv        = vault.createElement('cvv');

// Mount by CSS selector or HTMLElement reference
cardNumber.mount('#card-number');
expiry.mount('#expiry');
cvv.mount('#cvv');
You can also pass element options at creation time:
const cardNumber = vault.createElement('cardNumber', {
  style:       { base: { fontSize: '16px', color: '#1a1a2e' } },
  placeholder: '1234 1234 1234 1234',
});

createElement Options

vault.createElement(type: ElementType, options?: ElementOptions): OzElement
OptionTypeDescription
styleElementStyleConfigPer-element style overrides. See Styling.
placeholderstringPlaceholder text shown inside the field
disabledbooleanMount the field in a disabled state
loadTimeoutMsnumberTimeout (ms) before loaderror fires for this element. Default: 10000.

Tokenizing

Call createToken() after the user has filled all fields. The SDK transmits the field values directly to the Ozura Vault API — raw data never passes through your code.
document.getElementById('pay-btn').addEventListener('click', async (e) => {
  e.preventDefault();
  try {
    const { token, cvcSession, card, billing } = await vault.createToken({
      billing: {
        firstName: 'Jane',
        lastName:  'Smith',
        address: {
          line1:   '123 Main St',
          city:    'San Francisco',
          state:   'CA',
          zip:     '94102',
          country: 'US',
        },
      },
    });

    // token      — vault token, pass to your backend
    // cvcSession — CVC session ID; returned on success and required for cardSale
    // card.last4 — last 4 digits (safe to display)
    // card.brand — 'visa' | 'mastercard' | 'amex' | …
    // billing    — validated, normalized billing ready for cardSale

    await fetch('/api/charge', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, cvcSession, billing }),
    });
  } catch (err) {
    if (err instanceof OzError) {
      console.error(err.message, err.errorCode);
    }
  }
});

createToken options

vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>
OptionTypeDescription
billingBillingDetailsBilling details to validate, normalize, and include in the token record
firstNamestring(deprecated) Use billing.firstName instead
lastNamestring(deprecated) Use billing.lastName instead

TokenResponse

{
  token:       string;             // vault token
  cvcSession?: string;             // CVC session ID — present on success; pass to cardSale
  card?: {
    last4:    string;              // last 4 digits of card number
    brand:    string;              // 'visa' | 'mastercard' | 'amex' | 'discover' | …
    expMonth: string;              // '01'–'12'
    expYear:  string;              // '2026', '2027', …
  };
  billing?: BillingDetails;        // echoed back (validated + normalized) if you passed billing
}
cvcSession is always present on a successful tokenization. Although the TypeScript type is string | undefined, the SDK rejects the result before returning if cvcSession is absent — this would indicate a vault misconfiguration. Always forward both token and cvcSession to your charge endpoint:
const { token, cvcSession, billing } = await vault.createToken({ billing });
// cvcSession is guaranteed non-empty here
await fetch('/api/charge', {
  method: 'POST',
  body: JSON.stringify({ token, cvcSession, billing }),
});

BillingDetails

{
  firstName: string;               // required, 1–50 chars
  lastName:  string;               // required, 1–50 chars
  email?:    string;               // valid email, max 50 chars
  phone?:    string;               // E.164 format, e.g. '+15551234567'
  address?: {
    line1:   string;               // required
    line2?:  string;               // optional
    city:    string;               // required
    state:   string;               // required; US/CA normalized to 2-letter code
    zip:     string;               // required
    country: string;               // ISO 3166-1 alpha-2, e.g. 'US'
  };
}
The SDK normalizes state to its standard 2-letter abbreviation for US and Canadian addresses. Full names (e.g. "California", "British Columbia") and 2-letter codes are both accepted. For country: 'US', the state must be a valid US state, territory (PR, GU, VI, AS, MP), or military address code (AE, AP, AA) — other values are rejected. For country: 'CA', the state must be a valid Canadian province code.

Events

Each element emits events you can listen to with .on():
cardNumber.on('change', (event) => {
  // event.empty     — boolean: true when the field has no input
  // event.complete  — boolean: field has reached the required length/format (may still be invalid — check `valid`)
  // event.valid     — boolean: value passes full validation (e.g. Luhn check, expiry in future)
  // event.error     — string | undefined: human-readable error message
  // event.cardBrand — string: 'visa' | 'mastercard' | 'amex' | … (cardNumber only)
});

expiry.on('change', (event) => {
  // event.month — string: parsed month '01'–'12' (expirationDate only)
  // event.year  — string: parsed 2-digit year e.g. '27' (expirationDate only)
});
The expiry onChange event includes month and year values. These are parsed from the masked expiry input and delivered to your handler for display purposes (e.g. showing the detected expiry date). If your PCI scope requires zero cardholder data on the merchant page, do not read or log these fields.
cardNumber.on('focus', () => { /* field received focus */ });
cardNumber.on('blur',  (data) => { /* field lost focus; data = { empty, complete, valid, error } */ });
cardNumber.on('ready', () => { /* iframe loaded and is interactive */ });
cardNumber.on('loaderror', ({ elementType, error }) => { /* iframe failed to load */ });

Gating the submit button

The vault and each field iframe load independently. Gate your submit button on both:
  • Vault readiness — the onReady callback (fires when the vault is ready to tokenize)
  • Field readiness — each field’s 'ready' event (fires when the iframe loaded and is interactive) plus change events for completion
let vaultReady = false;
const fieldReady  = { cardNumber: false, expiry: false, cvv: false };
const fieldFilled = { cardNumber: false, expiry: false, cvv: false };

function updatePayButton() {
  const allReady = vaultReady &&
    fieldReady.cardNumber && fieldReady.expiry && fieldReady.cvv &&
    fieldFilled.cardNumber && fieldFilled.expiry && fieldFilled.cvv;
  document.getElementById('pay-btn').disabled = !allReady;
}

const vault = await OzVault.create({
  pubKey:      'YOUR_PUB_KEY',
  fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
  onReady: () => { vaultReady = true; updatePayButton(); },
});

cardNumber.on('ready', () => { fieldReady.cardNumber = true; updatePayButton(); });
expiry.on('ready',     () => { fieldReady.expiry = true;     updatePayButton(); });
cvv.on('ready',        () => { fieldReady.cvv = true;        updatePayButton(); });

cardNumber.on('change', ({ complete, valid }) => {
  fieldFilled.cardNumber = complete && valid; updatePayButton();
});
expiry.on('change', ({ complete, valid }) => {
  fieldFilled.expiry = complete && valid; updatePayButton();
});
cvv.on('change', ({ complete, valid }) => {
  fieldFilled.cvv = complete && valid; updatePayButton();
});

Teardown

When the checkout component unmounts, call vault.destroy() to remove all iframes and listeners. In vanilla JS use a cancel flag to handle the async nature of OzVault.create():
let cancelled = false;
let vault: OzVault | null = null;

OzVault.create({
  pubKey:      'YOUR_PUB_KEY',
  fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
}).then(v => {
  if (cancelled) { v.destroy(); return; }
  vault = v;
  // mount elements...
});

// On unmount:
function cleanup() {
  cancelled = true;
  vault?.destroy();
}

Auto-Advance Focus

Elements automatically advance focus from one field to the next on completion:
  • When card number is complete and valid → focus moves to expiry
  • When expiry is complete and valid → focus moves to CVV
This is built-in and requires no configuration.

Card Brand Detection

The cardNumber element detects the card brand as the user types and emits it via change:
cardNumber.on('change', ({ cardBrand }) => {
  // Update your card brand icon
  document.getElementById('brand-icon').src = `/icons/${cardBrand}.svg`;
});
Brand valueDescription
'visa'Visa
'mastercard'Mastercard
'amex'American Express (CVV becomes 4 digits)
'discover'Discover
'dinersclub'Diners Club
'jcb'JCB
'unknown'Not yet detected or unrecognized

Imperatively Managing Elements

// Focus a field programmatically
cardNumber.focus();

// Clear a field
cardNumber.clear();

// Check if the iframe is ready
if (cardNumber.isReady) { /* safe to proceed */ }

// Get the element type
cardNumber.type; // 'cardNumber'

// Tear down the iframe and remove listeners
cardNumber.destroy();

Full End-to-End Example (Vanilla JS + Express)

onReady fires before OzVault.create() resolves. The tokenizer iframe loads concurrently with your fetchWaxKey call, so the onReady callback can fire while await OzVault.create(...) is still pending — meaning vault is undefined inside the callback. Never reference vault inside onReady. Declare your readiness flags before calling create() so they exist when the callback runs. See the example below for the correct pattern.
A complete working example: HTML page with card fields, Express backend with /api/mint-wax and /api/charge routes, and the full tokenize → charge flow.

Frontend (HTML + JS)

<!DOCTYPE html>
<html>
<head>
  <title>Checkout</title>
  <style>
    .field-wrap { border: 1px solid #e5e7eb; border-radius: 6px; padding: 2px; margin-bottom: 12px; }
    .row        { display: flex; gap: 12px; }
    .row .field-wrap { flex: 1; }
    #error      { color: #ef4444; margin-top: 8px; min-height: 20px; }
  </style>
</head>
<body>
  <form id="payment-form">
    <div class="field-wrap" id="card-number-wrap"><div id="card-number"></div></div>
    <div class="row">
      <div class="field-wrap"><div id="expiry"></div></div>
      <div class="field-wrap"><div id="cvv"></div></div>
    </div>
    <p id="error"></p>
    <button id="pay-btn" type="submit" disabled>Pay $49.00</button>
  </form>

  <script type="module">
    import { OzVault, OzError, createFetchWaxKey } from '/oz-elements.esm.js';

    const errorEl  = document.getElementById('error');
    const payBtn   = document.getElementById('pay-btn');

    // — Readiness gating ————————————————————————————————————————
    // Declare all state BEFORE OzVault.create(). The onReady callback fires when
    // the tokenizer iframe loads, which can happen before create() resolves (the
    // tokenizer loads concurrently with your fetchWaxKey call). Variables must
    // exist when onReady runs. Do not reference `vault` inside onReady — it is
    // still undefined at that point.
    let vaultReady = false;
    const fieldReady  = { cardNumber: false, expiry: false, cvv: false };
    const fieldFilled = { cardNumber: false, expiry: false, cvv: false };

    function updatePayBtn() {
      payBtn.disabled = !(
        vaultReady &&
        fieldReady.cardNumber && fieldReady.expiry && fieldReady.cvv &&
        fieldFilled.cardNumber && fieldFilled.expiry && fieldFilled.cvv
      );
    }

    // — Vault initialization ————————————————————————————————————
    let vault;
    try {
      vault = await OzVault.create({
        pubKey:      'YOUR_PUB_KEY',  // use test key on localhost
        fetchWaxKey: createFetchWaxKey('/api/mint-wax'),
        onReady: () => {
          vaultReady = true;
          updatePayBtn();
        },
      });
    } catch (err) {
      errorEl.textContent = 'Payment fields could not load. Please refresh.';
      console.error('Vault init failed:', err);
      throw err;  // vault is undefined — stop execution
    }

    // — Mount fields ————————————————————————————————————————————
    const cardNumber = vault.createElement('cardNumber');
    const expiry     = vault.createElement('expirationDate');
    const cvv        = vault.createElement('cvv');

    cardNumber.mount('#card-number');
    expiry.mount('#expiry');
    cvv.mount('#cvv');

    ['cardNumber', 'expiry', 'cvv'].forEach(type => {
      const el   = { cardNumber, expiry, cvv }[type];
      const key  = type;
      el.on('ready',  () => { fieldReady[key]  = true;  updatePayBtn(); });
      el.on('change', ({ complete, valid }) => { fieldFilled[key] = complete && valid; updatePayBtn(); });
    });

    // — Tokenize + charge ———————————————————————————————————————
    document.getElementById('payment-form').addEventListener('submit', async (e) => {
      e.preventDefault();
      errorEl.textContent = '';
      payBtn.disabled = true;

      try {
        const { token, cvcSession, billing } = await vault.createToken({
          billing: {
            firstName: document.getElementById('first-name').value,
            lastName:  document.getElementById('last-name').value,
          },
        });

        const res = await fetch('/api/charge', {
          method:  'POST',
          headers: { 'Content-Type': 'application/json' },
          body:    JSON.stringify({ token, cvcSession, billing }),
        });

        if (!res.ok) {
          const { error } = await res.json();
          throw new Error(error || 'Payment failed');
        }

        window.location.href = '/success';

      } catch (err) {
        errorEl.textContent = err instanceof OzError ? err.message : 'Something went wrong.';
        payBtn.disabled = false;
      }
    });
  </script>
</body>
</html>

Backend (Express)

// server.js
import express from 'express';
import { Ozura, OzuraError, getClientIp, createMintWaxMiddleware } from '@ozura/elements/server';

const app = express();
app.use(express.json());

// Serve the SDK from the same origin for localhost CDN-free loading
app.get('/oz-elements.esm.js', (_, res) =>
  res.sendFile('node_modules/@ozura/elements/dist/oz-elements.esm.js', { root: '.' })
);

const ozura = new Ozura({
  merchantId: process.env.MERCHANT_ID,
  apiKey:     process.env.MERCHANT_API_KEY,
  vaultKey:   process.env.VAULT_API_KEY,
});

// Wax key route — called automatically by the frontend SDK
app.post('/api/mint-wax', createMintWaxMiddleware(ozura));

// Charge route — called after tokenization
app.post('/api/charge', async (req, res) => {
  try {
    const { token, cvcSession, billing } = req.body;

    // Always fetch the authoritative amount from your own database
    const amount = '49.00';

    const result = await ozura.cardSale({
      token,
      cvcSession,
      amount,
      currency:        'USD',
      billing,
      clientIpAddress: getClientIp(req),
    });

    // Store result.transactionId — required for refunds
    res.json({ success: true, transactionId: result.transactionId });

  } catch (err) {
    const status  = err instanceof OzuraError ? (err.statusCode || 500) : 500;
    const message = err instanceof OzuraError ? err.message : 'Internal server error';
    res.status(status).json({ error: message });
  }
});

app.listen(3000, () => console.log('Listening on http://localhost:3000'));
This example serves the SDK locally to avoid CDN CORS issues on localhost. In production, use the CDN URL or your bundler — remove the /oz-elements.esm.js static route. See Installation → CDN for details.

Test Cards

BrandNumberCVCExpiry
Visa4111 1111 1111 1111Any 3 digitsAny future date
Mastercard5555 5555 5555 4444Any 3 digitsAny future date
Amex3782 822463 10005Any 4 digitsAny future date
Testing your integration? Use the test pub key on localhost — see Installation → Local Development for setup details and test card numbers above.

Next Steps

Styling

Customize colors, fonts, and states.

React Components

OzElements provider and pre-built components.

Error Handling

Handle tokenization errors gracefully.

Server SDK

Process the token with cardSale() on your backend.