> ## 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

> Embed card number, expiry, and CVV fields and tokenize in the browser using OzVault.

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

| `type`           | Description                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `cardNumber`     | Full card number (13–19 digits) with Luhn validation, brand detection, and formatting (e.g. `xxxx xxxx xxxx xxxx` for 16-digit cards) |
| `expirationDate` | `MM / YY` expiry with future-date validation                                                                                          |
| `cvv`            | 3-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.

```ts theme={null}
import { OzVault, OzError } from '@ozura/elements';

let vault;
try {
  vault = await OzVault.create({
    pubKey:     'YOUR_PUB_KEY',         // omit for test vault keys
    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');
```

Each mounted field renders as a **46 px tall iframe**. The SDK sets that height automatically — you don't need to set a height on the wrapper `<div>`. Just make sure the wrapper has visible width and is not hidden with `overflow: hidden` or `height: 0`, or the field will be invisible.

You can also pass element options at creation time:

```ts theme={null}
const cardNumber = vault.createElement('cardNumber', {
  style:       { base: { fontSize: '16px', color: '#1a1a2e' } },
  placeholder: '1234 1234 1234 1234',
});
```

***

## createElement Options

```ts theme={null}
vault.createElement(type: ElementType, options?: ElementOptions): OzElement
```

| Option          | Type                 | Description                                                               |
| --------------- | -------------------- | ------------------------------------------------------------------------- |
| `style`         | `ElementStyleConfig` | Per-element style overrides. See [Styling](/sdks/elements/styling).       |
| `placeholder`   | `string`             | Placeholder text shown inside the field                                   |
| `disabled`      | `boolean`            | Mount the field in a disabled state                                       |
| `loadTimeoutMs` | `number`             | Timeout (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.

```ts theme={null}
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

```ts theme={null}
vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>
```

| Option      | Type             | Description                                                             |
| ----------- | ---------------- | ----------------------------------------------------------------------- |
| `billing`   | `BillingDetails` | Billing details to validate, normalize, and include in the token record |
| `firstName` | `string`         | *(deprecated)* Use `billing.firstName` instead                          |
| `lastName`  | `string`         | *(deprecated)* Use `billing.lastName` instead                           |

### TokenResponse

```ts theme={null}
{
  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
}
```

<Note>
  **`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:

  ```ts theme={null}
  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 }),
  });
  ```
</Note>

<Note>
  **`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.
</Note>

### BillingDetails

```ts theme={null}
{
  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.

<Warning>
  **`cardSale` requires more billing fields than tokenize-only.** The OzuraPay 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 OzuraPay 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), `billing` can be omitted entirely — or if you want billing details echoed back in `TokenResponse.billing`, pass at minimum `{ firstName, lastName }`.
</Warning>

***

## Events

Each element emits events you can listen to with `.on()`:

```ts theme={null}
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)
});
```

<Note>
  **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.
</Note>

```ts theme={null}
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

```ts theme={null}
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',           // omit for test vault keys
  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()`:

```ts theme={null}
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`:

```ts theme={null}
cardNumber.on('change', ({ cardBrand }) => {
  // Update your card brand icon
  document.getElementById('brand-icon').src = `/icons/${cardBrand}.svg`;
});
```

| Brand value    | Description                             |
| -------------- | --------------------------------------- |
| `'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

```ts theme={null}
// 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)

<Warning>
  **`onReady` can fire while `OzVault.create()` is still pending.** Vault initialization runs multiple steps in parallel. The `onReady` callback can fire while `await OzVault.create(...)` is still in progress — 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.
</Warning>

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)

```html theme={null}
<!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)

```js theme={null}
// 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'));
```

<Info>
  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](/sdks/elements/installation#cdn-script-tag) for details.
</Info>

***

## Test Cards

These cards are for testing the **vault tokenization flow** — they are Luhn-valid and accepted by the vault in test mode. Use them to confirm your fields mount, tokenize, and return a token correctly.

| Brand      | Number                | CVC          | Expiry          |
| ---------- | --------------------- | ------------ | --------------- |
| Visa       | `4111 1111 1111 1111` | Any 3 digits | Any future date |
| Mastercard | `5555 5555 5555 4444` | Any 3 digits | Any future date |
| Amex       | `3782 822463 10005`   | Any 4 digits | Any future date |

<Info>
  **Testing a full charge with OzuraPay?** Use the cards on the [Test Credentials](/guides/test-credentials) page instead — those are calibrated for OzuraPay's sandbox, where the outcome (approve / decline) is determined by the charge amount, not the card number.
</Info>

<Info>
  **Testing your integration?** Use the test pub key on localhost — see [Installation → Local Development](/sdks/elements/installation#local-development) for setup details and test card numbers above.
</Info>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Styling" icon="palette" href="/sdks/elements/styling">
    Customize colors, fonts, and states.
  </Card>

  <Card title="React Components" icon="atom" href="/sdks/elements/react">
    OzElements provider and pre-built components.
  </Card>

  <Card title="Error Handling" icon="triangle-alert" href="/sdks/elements/error-handling">
    Handle tokenization errors gracefully.
  </Card>

  <Card title="Server SDK" icon="server" href="/sdks/elements/server">
    Process the token with cardSale() on your backend.
  </Card>
</CardGroup>
