Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ozura.com/llms.txt

Use this file to discover all available pages before exploring further.

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 session is initialized; 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 } from '@ozura/elements';

let vault;
try {
  vault = await OzVault.create({
    pubKey:     'YOUR_PUB_KEY',
    sessionUrl: '/api/oz-session',
  });
} catch (err) {
  // Pub key blocked, session endpoint 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 — always 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. The SDK validates this field before resolving createToken() — if it is absent the promise rejects with an OzError (errorCode: 'server'), which 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 }),
});
cvcSession lifecycle. The cvcSession is a short-lived vault credential that allows your server to retrieve the CVC value during a cardSale call. It shares the parent session TTL (default 30 minutes). It is safe to briefly persist it server-side — e.g. in a Redis key with a short TTL, or in a signed server-session — between the tokenization call and the charge call (seconds to minutes is fine). Do not store it long-term or log it. The vault invalidates the CVC credential after it is consumed by cardSale, so it is single-use.

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.
cardSale requires more billing fields than tokenize-only. The Pay API enforces minLength: 1 on billing fields — passing an empty string "" causes a validation error even if the field is technically optional. The SDK strips absent optional fields automatically (omits the key rather than sending ""). However, in practice cardSale typically requires email, phone, and a full address to process the charge. If any of these are missing, the Pay API will return a validation error.Recommendation: For charge flows, collect email, phone, and full address from the customer and pass them in BillingDetails. For tokenize-only flows (no charging), firstName and lastName are the only fields required by the vault.

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',
  sessionUrl: '/api/oz-session',
  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',
  sessionUrl: '/api/oz-session',
}).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. Vault initialization runs multiple steps in parallel. 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/oz-session 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="row">
      <input id="first-name" type="text" placeholder="First name" autocomplete="given-name" />
      <input id="last-name"  type="text" placeholder="Last name"  autocomplete="family-name" />
    </div>
    <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 } 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 the session fetch). 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
        sessionUrl: '/api/oz-session',
        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) {
        vault.reset();                   // clear fields so customer can re-enter
        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, createSessionMiddleware } 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,
});

// Session route — called automatically by the frontend SDK
app.post('/api/oz-session', createSessionMiddleware(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.