npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ozura/elements

v1.4.0

Published

PCI-compliant card tokenization SDK for the Ozura Vault — collect card data in iframe-isolated fields and tokenize without PCI scope

Readme

@ozura/elements

PCI-isolated card and bank account tokenization SDK for the Ozura Vault.

Card data is collected inside Ozura-hosted iframes so raw numbers never touch your JavaScript bundle, your server logs, or your network traffic. The tokenizer communicates directly with the vault using MessageChannel port transfers — the merchant page acts as a layout host only.

📖 Full documentation: docs.ozura.com/sdks/elements/overview

The docs include step-by-step guides, interactive examples, a complete API reference, and styling walkthroughs. If you are starting a new integration, the docs are the best place to begin.


Table of contents


Full documentation

The README covers the most common integration patterns. The hosted docs at docs.ozura.com/sdks/elements go deeper on every topic:

| Page | URL | |---|---| | Overview | docs.ozura.com/sdks/elements/overview | | Installation & setup | docs.ozura.com/sdks/elements/installation | | Card elements | docs.ozura.com/sdks/elements/card-elements | | Bank elements | docs.ozura.com/sdks/elements/bank-elements | | React integration | docs.ozura.com/sdks/elements/react | | Styling | docs.ozura.com/sdks/elements/styling | | Error handling | docs.ozura.com/sdks/elements/error-handling | | Server SDK | docs.ozura.com/sdks/elements/server | | API reference | docs.ozura.com/sdks/elements/api-reference |


How it works

Merchant page
  ├── OzVault (manages tokenizer iframe + element iframes)
  ├── [hidden]  tokenizer-frame.html  ← Ozura origin
  ├── [visible] element-frame.html    ← card number   ─┐ MessageChannel
  ├── [visible] element-frame.html    ← expiry         ├─ port transfer
  └── [visible] element-frame.html    ← CVV           ─┘
  1. OzVault.create() mounts a hidden tokenizer iframe and fetches a short-lived session key from your server.
  2. Calling vault.createElement() mounts a visible input iframe for each field.
  3. vault.createToken() opens a direct MessageChannel between each element iframe and the tokenizer iframe. Raw values travel over those ports — they never pass through your JavaScript.
  4. The tokenizer POSTs directly to the vault API over HTTPS and returns a token to your page.

Your server only ever sees a token, never card data.


Credentials

| Credential | Format | Where it lives | Required for | |---|---|---|---| | Vault pub key | pk_live_… or pk_prod_… | Frontend env var (safe to expose) | All integrations | | Vault API key | key_… | Server env var — never in the browser | Creating sessions (all integrations) | | Pay API key | ak_… | Server env var only | OzuraPay merchants (card charging) | | Merchant ID | ozu_… | Server env var only | OzuraPay merchants (card charging) |

If you are not routing payments through OzuraPay you only need the vault pub key (frontend) and vault API key (backend).


Sandbox and testing

To test a full integration end-to-end without processing real money:

  1. Create a vault project — go to ozuravault.com, sign up, and create a Test project (the default for new projects).
  2. Get a test vault key — open the project, create an application, and copy the test key. Omit pubKey from OzVault.create() (or <OzElements>) when using a test key — no pub key is needed for test projects.
  3. Set up the session endpoint — implement /api/oz-session on your backend (see Server setup) using your test vault key as vaultKey.
  4. Add the SDK — install @ozura/elements and mount your card fields (see the Quick start sections below).
  5. Run the tokenize flow — use a test card number to fill the fields and call createToken(). You receive a vault token and a CVC session.
  6. Hit your processor's sandbox — pass the token and CVC session to your backend charge endpoint and forward them to your payment processor's sandbox environment. If you are routing through OzuraPay, use https://sandbox.payapi.v2.ozurapay.com with sandbox merchant credentials from the Ozura merchant dashboard.

Test card numbers: See Test Credentials for card numbers, expected outcomes, and the amount-based result model.


Installation

📖 Installation guide — npm, yarn, CDN setup, and TypeScript configuration.

npm install @ozura/elements

React and React DOM are peer dependencies (optional — only needed for @ozura/elements/react):

npm install react react-dom   # if not already installed

Requirements: Node ≥ 18, React ≥ 17 (React peer).


Quick start — React

📖 React integration guide — provider setup, pre-built components, hook reference, and full examples.

// 1. Wrap your checkout in <OzElements>
import { OzElements, OzCard, useOzElements } from '@ozura/elements/react';

function CheckoutPage() {
  return (
    <OzElements
      pubKey="pk_live_..."
      sessionUrl="/api/oz-session"
    >
      <CheckoutForm />
    </OzElements>
  );
}

// 2. Collect card data and tokenize
function CheckoutForm() {
  const { createToken, reset, ready } = useOzElements();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const { token, cvcSession, billing } = await createToken({
        billing: {
          firstName: 'Jane',
          lastName:  'Smith',
          email:     '[email protected]',
          address: { line1: '123 Main St', city: 'Austin', state: 'TX', zip: '78701', country: 'US' },
        },
      });

      // Send token to your server
      await fetch('/api/charge', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token, cvcSession, billing }),
      });
    } catch (err) {
      reset();               // clear fields so the customer can re-enter
      console.error(err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <OzCard onChange={(state) => console.log(state.cardBrand)} />
      <button type="submit" disabled={!ready}>Pay</button>
    </form>
  );
}

Quick start — Vanilla JS

📖 Card elements guide — full end-to-end example, field events, submit-button gating, and auto-advance behaviour.

import { OzVault } from '@ozura/elements';

// Declare state BEFORE OzVault.create(). The onReady callback fires when the
// tokenizer iframe loads — this can happen before create() resolves because the
// iframe loads concurrently with fetchWaxKey. At that moment, `vault` is still
// undefined. Do not reference `vault` inside onReady.
let readyCount = 0;
let tokenizerIsReady = false;

function checkReady() {
  if (readyCount === 3 && tokenizerIsReady) enablePayButton();
}

const vault = await OzVault.create({
  pubKey: 'pk_live_...',
  sessionUrl: '/api/oz-session',
  onReady: () => { tokenizerIsReady = true; checkReady(); },   // tokenizer iframe loaded
});

const cardNumberEl  = vault.createElement('cardNumber');
const expiryEl      = vault.createElement('expirationDate');
const cvvEl         = vault.createElement('cvv');

cardNumberEl.mount('#card-number');
expiryEl.mount('#expiry');
cvvEl.mount('#cvv');

[cardNumberEl, expiryEl, cvvEl].forEach(el => {
  el.on('ready', () => { readyCount++; checkReady(); });
});

async function pay() {
  try {
    const { token, cvcSession, card } = await vault.createToken({
      billing: { firstName: 'Jane', lastName: 'Smith' },
    });
    // POST { token, cvcSession } to your server
  } catch (err) {
    vault.reset();           // clear fields; let customer re-enter
    console.error(err);
  }
}

// Clean up when done
vault.destroy();

Gating the submit button in vanilla JS requires checking both vault.isReady (tokenizer iframe loaded) and every element's ready event (input iframes loaded). In React, useOzElements().ready combines both automatically.


Server setup

📖 Server SDK guide — session creation, card sale handler factories, IP extraction, and manual implementation patterns.

Session endpoint

The SDK calls your session endpoint whenever it needs to start or refresh a payment session. Your backend creates a short-lived session key from the vault using your vault API key — the key never touches the browser.

Next.js App Router (recommended)

// app/api/oz-session/route.ts
import { Ozura, createSessionHandler } from '@ozura/elements/server';

const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });

export const POST = createSessionHandler(ozura);

Express

import express from 'express';
import { Ozura, createSessionMiddleware } from '@ozura/elements/server';

const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
const app = express();

app.use(express.json());
app.post('/api/oz-session', createSessionMiddleware(ozura));

Manual implementation (for custom logic or auth checks)

// POST /api/oz-session
const { sessionId } = await req.json();
const { sessionKey } = await ozura.createSession({ sessionId });
return Response.json({ sessionKey });

Card sale endpoint

After createToken() resolves on the frontend, POST { token, cvcSession, billing } to your server to charge the card.

Next.js App Router

// app/api/charge/route.ts
import { Ozura, createCardSaleHandler } from '@ozura/elements/server';

const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });

export const POST = createCardSaleHandler(ozura, {
  getAmount: async (body) => {
    const order = await db.orders.findById(body.orderId as string);
    return order.total; // decimal string, e.g. "49.00"
  },
  // getCurrency: async (body) => 'USD',  // optional, defaults to "USD"
});

Express

app.post('/api/charge', createCardSaleMiddleware(ozura, {
  getAmount: async (body) => {
    const order = await db.orders.findById(body.orderId as string);
    return order.total; // decimal string, e.g. "49.00"
  },
  // getCurrency: async (body) => 'USD',  // optional, defaults to "USD"
}));

createCardSaleMiddleware always terminates the request — it does not call next() and cannot be composed in a middleware chain.

Manual implementation

const { token, cvcSession, billing } = await req.json();
const result = await ozura.cardSale({
  token,
  cvcSession,
  amount: '49.00',
  currency: 'USD',
  billing,
  // Take the first IP from the forwarded-for chain; fall back to socket address.
  // req is a Fetch API Request (Next.js App Router / Vercel Edge).
  clientIpAddress: req.headers.get('x-forwarded-for')?.split(',')[0].trim()
    ?? req.headers.get('x-real-ip')
    ?? '',
});
// result.transactionId, result.amount, result.cardLastFour, result.cardBrand

Vanilla JS API

📖 API reference — complete type definitions and method signatures for every class.

OzVault.create(options)

const vault = await OzVault.create(options: VaultOptions): Promise<OzVault>

Mounts the hidden tokenizer iframe and fetches a session key concurrently. Both happen in parallel — by the time create() resolves, the iframe may already be ready.

| Option | Type | Required | Description | |---|---|---|---| | pubKey | string | — ² | Your public key from the Ozura admin. Required for production vault keys (pk_live_… / pk_prod_…). Omit when using a test vault key from a Test project at ozuravault.com — the vault recognises test keys and tokenizes without the X-Pub-Key header. The SDK will emit a one-time console.warn when pubKey is omitted to make this explicit. | | sessionUrl | string | ✓ ¹ | URL of your session endpoint. The simplest option — pass the path and the SDK handles everything. | | getSessionKey | (sessionId: string) => Promise<string> | ✓ ¹ | Custom async callback for obtaining the session key. Use when you need custom headers or auth logic. | | fetchWaxKey | (sessionId: string) => Promise<string> | ✓ ¹ | Deprecated. Use sessionUrl or getSessionKey instead. | | frameBaseUrl | string | — | Base URL for iframe assets. Defaults to production CDN. Override for local dev (see Local development). | | fonts | FontSource[] | — | Custom fonts to inject into all element iframes. | | appearance | Appearance | — | Global theme and variable overrides. | | loadTimeoutMs | number | — | Tokenizer iframe load timeout in ms. Default: 10000. Only takes effect when onLoadError is also provided. | | onLoadError | () => void | — | Called if the tokenizer iframe fails to load within loadTimeoutMs. | | onSessionRefresh | () => void | — | Called when the SDK silently refreshes the session mid-tokenization (key expired or consumed). | | onReady | () => void | — | Called once when the tokenizer iframe has loaded and is ready. Use in vanilla JS to re-check submit-button readiness. In React, useOzElements().ready handles this automatically. | | sessionLimit | number | — | Card submissions allowed per session before the SDK refreshes automatically. Default: 3. Must match sessionLimit in your server-side createSession call. | | debug | boolean | — | Enables structured [OzVault]-prefixed console.log output at every lifecycle event. Safe to use in production — no sensitive data is ever logged. Default: false. See Debug mode for details. |

¹ Exactly one of sessionUrl, getSessionKey, or fetchWaxKey is required.

² pubKey is required for production vault keys and optional for test vault keys from a Test project on the vault. If you provide it, it must be a non-empty string; if you don't have one (test-mode flow), omit the option entirely rather than passing ''.

Throws OzError if the session fetch rejects, returns an empty string, or returns a non-string value.

sessionUrl retry behavior: The SDK enforces a 10-second per-attempt timeout and retries once after 750 ms on pure network failures (connection refused, DNS failure, offline). HTTP 4xx/5xx errors are never retried — they signal endpoint misconfiguration or invalid credentials and require developer action. Errors are thrown as OzError instances so you can inspect err.errorCode.


vault.createElement(type, options?)

vault.createElement(type: ElementType, options?: ElementOptions): OzElement

Creates and returns an element iframe. Call .mount(target) to attach it to the DOM.

ElementType: 'cardNumber' | 'cvv' | 'expirationDate'

const cardEl = vault.createElement('cardNumber', {
  placeholder: '1234 5678 9012 3456',
  style: {
    base:     { color: '#1a1a1a', fontSize: '16px' },
    focus:    { borderColor: '#6366f1' },
    invalid:  { color: '#dc2626' },
    complete: { color: '#16a34a' },
  },
});

cardEl.mount('#card-number-container');

ElementOptions:

| Option | Type | Description | |---|---|---| | style | ElementStyleConfig | Per-state style overrides. See Styling. | | placeholder | string | Placeholder text (max 100 characters). | | disabled | boolean | Disables the input. | | loadTimeoutMs | number | Iframe load timeout in ms. Default: 10000. |

OzElement methods:

| Method | Description | |---|---| | .mount(target) | Mount the iframe. Accepts a CSS selector string or HTMLElement. | | .unmount() | Remove the iframe from the DOM. The element can be re-mounted. | | .destroy() | Permanently destroy the element. Cannot be re-mounted. | | .update(options) | Update placeholder, style, or disabled state without re-mounting. Merge semantics — only provided keys are applied; omitted keys retain their values. To reset a property, pass it with an empty string (e.g. { style: { base: { color: '' } } }). To fully reset all styles, destroy and recreate the element. | | .clear() | Clear the field value. | | .focus() | Programmatically focus the input. | | .blur() | Programmatically blur the input. | | .on(event, fn) | Subscribe to an event. Returns this for chaining. | | .off(event, fn) | Remove an event handler. | | .once(event, fn) | Subscribe for a single invocation. | | .isReady | true once the iframe has loaded and signalled ready. |


vault.createToken(options?)

vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>

Tokenizes all mounted and ready card elements. Raw values travel directly from element iframes to the tokenizer iframe via MessageChannel — they never pass through your page's JavaScript.

Returns a TokenResponse:

interface TokenResponse {
  token:      string;           // Vault token — pass to cardSale
  cvcSession: string;           // CVC session — always present; pass to cardSale
  card?: {                      // Card metadata — present when vault returns all four fields
    last4:    string;           // e.g. "4242"
    brand:    string;           // e.g. "visa"
    expMonth: string;           // e.g. "09"
    expYear:  string;           // e.g. "2027"
  };
  billing?: BillingDetails;     // Normalized billing — only present if billing was passed in
}

TokenizeOptions:

| Option | Type | Description | |---|---|---| | billing | BillingDetails | Validated and normalized billing details. Returned in TokenResponse.billing. | | firstName | string | Deprecated. Pass inside billing instead. | | lastName | string | Deprecated. Pass inside billing instead. |

Throws OzError if:

  • The vault is not ready (errorCode: 'unknown')
  • A tokenization is already in progress
  • Billing validation fails (errorCode: 'validation')
  • No elements are mounted
  • The vault returns an error (errorCode reflects the HTTP status)
  • The request times out after 30 seconds (errorCode: 'timeout') — this timeout is separate from loadTimeoutMs and is not configurable

vault.tokenizeCount

vault.tokenizeCount: number  // read-only getter

Returns the number of successful createToken() / createBankToken() calls made in the current session. Resets to 0 each time the session is refreshed (proactively or reactively). Use this in vanilla JS to display "attempts remaining" feedback or gate the submit button:

const MAX = 3; // matches maxTokenizeCalls
const remaining = MAX - vault.tokenizeCount;
payButton.textContent = `Pay (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`;

In React, use tokenizeCount from useOzElements() instead — it is a reactive state value and will trigger re-renders automatically.


vault.createBankElement()

vault.createBankElement(type: BankElementType, options?: ElementOptions): OzElement

Creates a bank account element. BankElementType: 'accountNumber' | 'routingNumber'.

const accountEl = vault.createBankElement('accountNumber');
const routingEl = vault.createBankElement('routingNumber');
accountEl.mount('#account-number');
routingEl.mount('#routing-number');

vault.createBankToken(options)

vault.createBankToken(options: BankTokenizeOptions): Promise<BankTokenResponse>

Tokenizes the mounted accountNumber and routingNumber elements. Both must be mounted and ready.

interface BankTokenizeOptions {
  firstName: string;  // Account holder first name (required, max 50 chars)
  lastName:  string;  // Account holder last name (required, max 50 chars)
}

interface BankTokenResponse {
  token: string;
  bank?: {
    last4:               string;  // Last 4 digits of account number
    routingNumberLast4:  string;
  };
}

Note: OzuraPay does not currently support bank account payments. Use the bank token with your own ACH processor. Bank tokens can be passed to any ACH-capable processor directly.

Card tokens and processors: Card tokenization returns a cvcSession alongside the token. OzuraPay's charge API requires cvcSession. If you are routing to a non-Ozura processor, pass the token directly — your processor's documentation will tell you whether a CVC session token is required.


vault.destroy()

vault.destroy(): void

Tears down the vault: removes all element and tokenizer iframes, clears the message event listener, rejects any pending createToken() / createBankToken() promises, and cancels active timeout handles. Call this when the checkout component unmounts.

// React useEffect cleanup — the cancel flag prevents a vault from leaking
// if the component unmounts before OzVault.create() resolves.
useEffect(() => {
  let cancelled = false;
  let vault: OzVault | null = null;
  OzVault.create(options).then(v => {
    if (cancelled) { v.destroy(); return; }
    vault = v;
  });
  return () => {
    cancelled = true;
    vault?.destroy();
  };
}, []);

vault.reset()

vault.reset(): void

Clears all mounted card and bank element fields without destroying the vault, refreshing the session, or resetting the tokenization budget. Call this after a declined payment to let the customer re-enter their card details on the same checkout screen.

The session key, its remaining budget, and all iframes are fully preserved — no network calls are made.

Session model: One session covers the full checkout. The default sessionLimit: 3 is enough for two declines and a final attempt. Use vault.reset() between declines — not vault.destroy() + recreate, which would waste the remaining budget and cause iframe flicker.

try {
  const { token, cvcSession } = await vault.createToken({ billing });
  await fetch('/api/charge', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token, cvcSession }),
  });
} catch (err) {
  vault.reset();                   // clear fields; let customer re-enter
  showError(err instanceof OzError ? err.message : 'Payment failed.');
}

vault.debugState()

vault.debugState(): Record<string, unknown>

Returns a structured snapshot of the vault's internal state. Always available regardless of whether debug: true is set. Useful for attaching to support tickets or dumping on error.

console.log(vault.debugState());
// {
//   vaultId: 'vault_abc12...',
//   isReady: true,
//   tokenizing: null,
//   destroyed: false,
//   waxKeyPresent: true,
//   tokenizeSuccessCount: 1,
//   maxTokenizeCalls: 3,
//   resetCount: 0,
//   elements: ['cardNumber', 'expirationDate', 'cvv'],
//   bankElements: [],
//   completionState: { 'a1b2c3d4': true, 'e5f6a7b8': true, '...' : false },
//   pendingTokenizations: 0,
//   pendingBankTokenizations: 0,
// }

No sensitive data is returned: wax keys, tokens, CVC sessions, and billing fields are never included.


OzElement events

element.on('change', (event: ElementChangeEvent) => { ... });
element.on('focus',  () => { ... });
element.on('blur',   () => { ... });
element.on('ready',  () => { ... });
element.on('loaderror', (payload: { elementType: string; error: string }) => { ... });

ElementChangeEvent:

| Field | Type | Description | |---|---|---| | empty | boolean | true when the field is empty. | | complete | boolean | true when the field has enough digits to be complete. | | valid | boolean | true when the value passes all validation (Luhn, expiry date, etc.). | | error | string \| undefined | User-facing error message when valid is false and the field has been touched. | | cardBrand | string \| undefined | Detected brand — only on cardNumber fields (e.g. "visa", "amex"). | | month | string \| undefined | Parsed 2-digit month — only on expirationDate fields. | | year | string \| undefined | Parsed 2-digit year — only on expirationDate fields. |

Expiry data and PCI scope: The expiry onChange event delivers parsed month and year to your handler for display purposes (e.g. "Card expires MM/YY"). These are not PANs or CVVs, but they are cardholder data. If your PCI scope requires zero cardholder data on the merchant page, do not read, log, or store these fields.

Auto-advance is built in: the vault automatically moves focus from card number → expiry → CVV when each field completes. No additional code required.


React API

📖 React guideOzElements provider, useOzElements hook, pre-built OzCard and OzBankCard components, and StrictMode behaviour.

OzElements provider

import { OzElements } from '@ozura/elements/react';

<OzElements
  pubKey="pk_live_..."
  sessionUrl="/api/oz-session"
  appearance={{ theme: 'flat', variables: { colorPrimary: '#6366f1' } }}
  onLoadError={() => setPaymentUnavailable(true)}
>
  {children}
</OzElements>

All VaultOptions are accepted as props. The provider creates a single OzVault instance and destroys it on unmount.

Prop changes and vault lifecycle: Changing pubKey, sessionUrl, frameBaseUrl, loadTimeoutMs, appearance, fonts, or sessionLimit destroys the current vault and creates a new one — all field iframes will remount. Changing getSessionKey, fetchWaxKey, onLoadError, onSessionRefresh, or onReady updates the callback in place via refs without recreating the vault.

One card form per provider: A vault holds one element per field type (cardNumber, expiry, cvv, etc.). Rendering two <OzCard> components under the same <OzElements> provider will cause the second to silently replace the first's iframes, breaking the first form. If you genuinely need two independent card forms on the same page, wrap each in its own <OzElements> provider with separate pubKey / sessionUrl configurations.


OzCard

Drop-in combined card component. Renders card number, expiry, and CVV with a configurable layout.

import { OzCard } from '@ozura/elements/react';

<OzCard
  layout="default"       // "default" (number on top, expiry+CVV below) | "rows" (stacked)
  onChange={(state) => {
    // state.complete   — all three fields complete + valid
    // state.cardBrand  — detected brand
    // state.error      — first error across all fields
    // state.fields     — per-field ElementChangeEvent objects
  }}
  onReady={() => console.log('all card fields loaded')}
  disabled={isSubmitting}
  labels={{ cardNumber: 'Card Number', expiry: 'Expiry', cvv: 'CVV' }}
  placeholders={{ cardNumber: '1234 5678 9012 3456', expiry: 'MM/YY', cvv: '···' }}
/>

OzCardProps (full):

| Prop | Type | Description | |---|---|---| | layout | 'default' \| 'rows' | 'default': number full-width, expiry+CVV side by side. 'rows': all stacked. | | gap | number \| string | Gap between fields. Default: 8 (px). | | style | ElementStyleConfig | Shared style applied to all three inputs. | | styles | { cardNumber?, expiry?, cvv? } | Per-field overrides merged on top of style. | | classNames | { cardNumber?, expiry?, cvv?, row? } | CSS class names for field wrappers and the expiry+CVV row. | | labels | { cardNumber?, expiry?, cvv? } | Optional label text above each field. | | labelStyle | React.CSSProperties | Style applied to all <label> elements. | | labelClassName | string | Class applied to all <label> elements. | | placeholders | { cardNumber?, expiry?, cvv? } | Custom placeholder text per field. | | hideErrors | boolean | Suppress the built-in error display. Handle via onChange. | | errorStyle | React.CSSProperties | Style for the built-in error container. | | errorClassName | string | Class for the built-in error container. | | renderError | (error: string) => ReactNode | Custom error renderer. | | onChange | (state: OzCardState) => void | Fires on any field change. | | onReady | () => void | Fires once all three iframes have loaded. | | onFocus | (field: 'cardNumber' \| 'expiry' \| 'cvv') => void | | | onBlur | (field: 'cardNumber' \| 'expiry' \| 'cvv') => void | | | disabled | boolean | Disable all inputs. | | className | string | Class for the outer wrapper. |


Individual field components

For custom layouts where OzCard is too opinionated:

import { OzCardNumber, OzExpiry, OzCvv } from '@ozura/elements/react';

<OzCardNumber onChange={handleChange} placeholder="Card number" />
<OzExpiry     onChange={handleChange} />
<OzCvv        onChange={handleChange} />

All accept OzFieldProps:

| Prop | Type | Description | |---|---|---| | style | ElementStyleConfig | Input styles. | | placeholder | string | Placeholder text. | | disabled | boolean | Disables the input. | | loadTimeoutMs | number | Iframe load timeout in ms. | | onChange | (event: ElementChangeEvent) => void | | | onFocus | () => void | | | onBlur | () => void | | | onReady | () => void | | | onLoadError | (error: string) => void | | | className | string | Class for the outer wrapper div. |


OzBankCard

import { OzBankCard } from '@ozura/elements/react';

<OzBankCard
  onChange={(state) => {
    // state.complete, state.error, state.fields.accountNumber, state.fields.routingNumber
  }}
  labels={{ accountNumber: 'Account Number', routingNumber: 'Routing Number' }}
/>

Or use individual bank components:

import { OzBankAccountNumber, OzBankRoutingNumber } from '@ozura/elements/react';

<OzBankAccountNumber onChange={handleChange} />
<OzBankRoutingNumber onChange={handleChange} />

useOzElements()

const { createToken, createBankToken, reset, ready, initError, tokenizeCount } = useOzElements();

Must be called from inside an <OzElements> provider tree.

| Return | Type | Description | |---|---|---| | createToken | (options?: TokenizeOptions) => Promise<TokenResponse> | Tokenize mounted card elements. | | createBankToken | (options: BankTokenizeOptions) => Promise<BankTokenResponse> | Tokenize mounted bank elements. | | reset | () => void | Clear all mounted element fields without destroying the vault or refreshing the session. Call after a declined payment so the customer can re-enter their details. | | ready | boolean | true when the tokenizer and all mounted element iframes are ready. Gate your submit button on this. See note below. | | initError | Error \| null | Non-null if OzVault.create() failed (e.g. session endpoint unreachable). Render a fallback UI. | | tokenizeCount | number | Number of successful tokenizations in the current session. Resets on session refresh or provider re-init. Useful for tracking calls against sessionLimit. |

ready vs vault.isReady: ready from useOzElements() is a composite — it combines vault.isReady (tokenizer loaded) with element readiness (all mounted input iframes loaded). vault.isReady alone is insufficient for gating a submit button. Always use ready from useOzElements() in React.


Vue API

📖 Vue 3 integration using the Composition API. Requires vue >= 3.3. No .vue SFC files needed in your setup — components work with <script setup> or the Options API.

Installation

npm install @ozura/elements vue

Quick start

useOzElements() calls Vue's inject() under the hood, so it must be called from a child of <OzElements>, not from the same component that renders it. Use two components — an outer wrapper that provides <OzElements>, and an inner form component that calls the composable:

CheckoutPage.vue — renders the provider

<script setup lang="ts">
import { OzElements } from '@ozura/elements/vue';
import CheckoutForm from './CheckoutForm.vue';
</script>

<template>
  <OzElements pub-key="pk_live_..." session-url="/api/oz-session">
    <CheckoutForm />
  </OzElements>
</template>

CheckoutForm.vue — child component, calls the composable

<script setup lang="ts">
import { OzCardNumber, OzExpiry, OzCvv, useOzElements } from '@ozura/elements/vue';

const { createToken, ready } = useOzElements();

async function handlePay() {
  const { token, cvcSession } = await createToken({
    billing: { firstName: 'Jane', lastName: 'Doe' },
  });
  // send token to your backend
}
</script>

<template>
  <OzCardNumber @change="(e) => console.log(e.cardBrand)" />
  <OzExpiry />
  <OzCvv />
  <button :disabled="!ready" @click="handlePay">Pay</button>
</template>

Individual fields

| Component | Element type | |---|---| | <OzCardNumber> | Card number | | <OzExpiry> | Expiry date | | <OzCvv> | CVV / CVC | | <OzBankAccountNumber> | Bank account number | | <OzBankRoutingNumber> | Bank routing number |

<OzElements> props

| Prop | Type | Description | |---|---|---| | pubKey | string | System public key from Ozura admin. Required for production vault keys; omit when using a test vault key from a Test project at ozuravault.com. | | sessionUrl | string | URL of your backend session endpoint (simplest path). | | getSessionKey | (sessionId: string) => Promise<string> | Custom async function for session key. Use instead of sessionUrl when you need custom headers or auth. | | frameBaseUrl | string | Override the default frame host (elements.ozura.com). | | fonts | FontSource[] | Custom fonts injected into element iframes. | | appearance | Appearance | Global theme and variable overrides applied to all elements. | | loadTimeoutMs | number | Iframe load timeout in ms. Default: 10 000. | | debug | boolean | Enable structured [OzVault] debug logging. Default: false. |

Event: @ready — emitted once when the vault and all mounted field iframes are ready.

useOzElements() return values

| Value | Type | Description | |---|---|---| | createToken | (options?: TokenizeOptions) => Promise<TokenResponse> | Tokenize mounted card fields. | | createBankToken | (options?: BankTokenizeOptions) => Promise<BankTokenResponse> | Tokenize mounted bank fields. | | ready | ComputedRef<boolean> | true when vault and all field iframes are ready. Gate your submit button on this. | | initError | Ref<Error \| null> | Non-null if OzVault.create() failed. Render a fallback UI. | | tokenizeCount | Ref<number> | Successful tokenizations in the current session. Resets on session refresh. | | reset | () => void | Clear all fields without destroying the vault. Call after a declined payment. |

Field props and events

All five field components share the same props and emits:

Props:

| Prop | Type | Description | |---|---|---| | placeholder | string | Override the default placeholder text. | | disabled | boolean | Disable the input. | | style | ElementStyleConfig | CSS-in-JS style object applied to the inner input. See Styling. |

Emits:

| Event | Payload | Description | |---|---|---| | @change | ElementChangeEvent | Fires on every keystroke with validation state and metadata. | | @focus | void | Fires when the field gains focus. | | @blur | void | Fires when the field loses focus. |


Styling

📖 Styling guide — full property allowlist, theme variables, appearance options, custom fonts, and var() limitations.

Per-element styles

Styles apply inside each iframe's <input> element. Only an explicit allowlist of CSS properties is accepted (typography, spacing, borders, box-shadow, cursor, transitions). Values containing url(), var(), expression(), javascript:, or CSS breakout characters are blocked on both the SDK and iframe sides. Use literal values instead of CSS custom properties (var(--token) is rejected). When a value is stripped, the browser falls back to the element's default (unstyled) appearance for that property — the failure is silent with no error or console warning. Resolve design tokens to literal values before passing them in.

const style: ElementStyleConfig = {
  base: {
    color:           '#1a1a1a',
    fontSize:        '16px',
    fontFamily:      '"Inter", sans-serif',
    padding:         '10px 12px',
    backgroundColor: '#ffffff',
    borderRadius:    '6px',
    border:          '1px solid #d1d5db',
  },
  focus: {
    borderColor: '#6366f1',
    boxShadow:   '0 0 0 3px rgba(99,102,241,0.15)',
    outline:     'none',
  },
  invalid: {
    borderColor: '#ef4444',
    color:       '#dc2626',
  },
  complete: {
    borderColor: '#22c55e',
  },
  placeholder: {
    color: '#9ca3af',
  },
};

State precedence: placeholder applies to the ::placeholder pseudo-element. focus, invalid, and complete merge on top of base.

Global appearance

Apply a preset theme and/or variable overrides to all elements at once:

// OzVault.create
const vault = await OzVault.create({
  pubKey: '...',
  sessionUrl: '/api/oz-session',
  appearance: {
    theme: 'flat',                    // 'default' | 'night' | 'flat'
    variables: {
      colorText:        '#1a1a1a',
      colorBackground:  '#ffffff',
      colorPrimary:     '#6366f1',    // focus caret + color
      colorDanger:      '#dc2626',    // invalid state
      colorSuccess:     '#16a34a',    // complete state
      colorPlaceholder: '#9ca3af',
      fontFamily:       '"Inter", sans-serif',
      fontSize:         '15px',
      fontWeight:       '400',
      padding:          '10px 14px',
      letterSpacing:    '0.01em',
    },
  },
});

// React provider
<OzElements pubKey="..." sessionUrl="..." appearance={{ theme: 'night' }}>

Per-element style takes precedence over appearance variables.

appearance: {} is not "no theme": Passing appearance: {} or appearance: { variables: { ... } } without a theme key applies the 'default' preset as a base. To render elements with no preset styling, omit appearance entirely and use per-element style overrides.

| appearance value | Result | |---|---| | (omitted entirely) | No preset — element uses minimal built-in defaults | | {} | Equivalent to { theme: 'default' } — full default theme applied | | { theme: 'night' } | Night theme | | { variables: { colorText: '#333' } } | Default theme + variable overrides |

Custom fonts

Fonts are injected into each iframe so they render inside the input fields:

fonts: [
  // Google Fonts or any HTTPS CSS URL
  { cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap' },

  // Custom @font-face
  {
    family:  'BrandFont',
    src:     'url(https://cdn.example.com/brand-font.woff2)',
    weight:  '400',
    style:   'normal',
    display: 'swap',
  },
]

Font src values must start with url(https://...). HTTP and data URIs are rejected.


Billing details

interface BillingDetails {
  firstName: string;   // 1–50 characters
  lastName:  string;   // 1–50 characters
  email?:    string;   // Valid email, max 50 characters
  phone?:    string;   // E.164 format, e.g. "+15551234567", max 50 characters
  address?: {
    line1:   string;   // 1–50 characters
    line2?:  string;   // Optional, omitted from cardSale if blank
    city:    string;   // 1–50 characters
    state:   string;   // For US/CA: normalized to 2-letter abbreviation
    zip:     string;   // 1–50 characters
    country: string;   // ISO 3166-1 alpha-2, e.g. "US"
  };
}

Billing is validated and normalized by both vault.createToken() and the server-side handler factories. TokenResponse.billing contains the normalized result ready to spread into a cardSale call.


Error handling

📖 Error handling guideOzError fields, every errorCode value, retry guidance, and session expiry behaviour.

All SDK errors are instances of OzError:

import { OzError } from '@ozura/elements';

try {
  const { token } = await vault.createToken({ billing });
} catch (err) {
  if (err instanceof OzError) {
    switch (err.errorCode) {
      case 'network':     // Connection failure — show retry UI
      case 'timeout':     // 30s deadline exceeded — safe to retry
      case 'server':      // 5xx from vault — transient, safe to retry
        if (err.retryable) showRetryPrompt();
        break;

      case 'auth':        // Bad pub key / API key — configuration issue
      case 'validation':  // Bad card data — show field-level error
      case 'config':      // frameBaseUrl not in permitted allowlist
      case 'unknown':
        showError(err.message);
        break;
    }
  }
}

OzError fields:

| Field | Type | Description | |---|---|---| | message | string | Human-readable, consumer-facing error message. | | errorCode | OzErrorCode | 'network' \| 'timeout' \| 'auth' \| 'validation' \| 'server' \| 'config' \| 'unknown' | | raw | string | Raw error string from the vault API, if available. | | retryable | boolean | true for network, timeout, server. false for auth, validation, config, unknown. |

Session expiry is handled automatically. When a session expires or is consumed between initialization and the user clicking Pay, the SDK silently fetches a fresh session and retries the tokenization once. You will only receive an auth error if the session refresh itself fails — for example, if your /api/oz-session backend endpoint is unreachable. A healthy auth error in production means your session endpoint needs attention, not that the user's card is bad.

Error normalisation helpers (for displaying errors from cardSale to users):

import { normalizeVaultError, normalizeBankVaultError, normalizeCardSaleError } from '@ozura/elements';

// Maps vault tokenize error strings to user-facing copy
const display = normalizeVaultError(err.raw);       // card flows
const display = normalizeBankVaultError(err.raw);   // bank/ACH flows
const display = normalizeCardSaleError(err.message); // cardSale API errors

Debug mode

Pass debug: true in VaultOptions (or as a prop on <OzElements>) to activate structured console logging at every SDK lifecycle event.

const vault = await OzVault.create({
  pubKey: 'pk_live_...',
  sessionUrl: '/api/oz-session',
  debug: true,   // enables [OzVault] console.log output
});
// React
<OzElements pubKey="pk_live_..." sessionUrl="/api/oz-session" debug>
  ...
</OzElements>

Each log entry is a [OzVault] <message> prefixed console.log call. Events logged include:

| Event | When it fires | |---|---| | vault created | Constructor completes | | wax key received | fetchWaxKey resolves | | mounting tokenizer iframe | Tokenizer iframe creation begins | | tokenizer iframe ready | Tokenizer iframe handshake complete | | element iframe ready | Each card/bank input iframe loads | | field changed | Per-field change event (empty/complete/valid/auto-advance state) | | auto-advance | Focus moves automatically between card fields | | createToken() called | Entry to createToken() | | OZ_TOKENIZE sent | Tokenize request dispatched to iframe | | token received | Token result returned (with elapsed ms) | | token error | Vault or network error during tokenize | | proactive wax key refresh triggered | Budget exhausted; refresh starting | | wax key refresh started/succeeded/failed | Refresh lifecycle | | tab hidden / tab visible | visibilitychange events | | reset() called | vault.reset() entry | | destroy() called | vault.destroy() entry |

Security: No sensitive data is ever logged. Wax keys, tokens, CVC sessions, and billing fields appear only as boolean presence flags (waxKeyPresent: true). Frame IDs and request IDs are truncated.

vault.debugState()

vault.debugState() is always available — regardless of whether debug: true was set — and returns a one-time snapshot for attaching to bug reports:

console.log(vault.debugState());

Sample output:

{
  "vaultId": "vault_abc12...",
  "isReady": true,
  "tokenizing": null,
  "destroyed": false,
  "waxKeyPresent": true,
  "tokenizeSuccessCount": 1,
  "maxTokenizeCalls": 3,
  "resetCount": 0,
  "elements": ["cardNumber", "expirationDate", "cvv"],
  "bankElements": [],
  "completionState": { "a1b2c3d4": true, "e5f6a7b8": true, "c9d0e1f2": false },
  "pendingTokenizations": 0,
  "pendingBankTokenizations": 0
}

Server utilities

📖 Server SDK guideOzura class methods, route handler factories, getClientIp, error types, and rate limits.

Ozura class

import { Ozura, OzuraError } from '@ozura/elements/server';

const ozura = new Ozura({
  merchantId: process.env.MERCHANT_ID!,
  apiKey:     process.env.MERCHANT_API_KEY!,
  vaultKey:   process.env.VAULT_API_KEY!,
  // apiUrl:   'https://api.ozura.com',  // override Pay API URL
  // vaultUrl: 'https://vault.ozura.com', // override vault URL
  timeoutMs: 30000,    // default
  retries:   2,        // max retry attempts for 5xx/network errors (3 total attempts)
});

Tokenize-only integrations (session creation + tokenize cards, no charging) only need vaultKey. The merchantId and apiKey fields are optional — they are validated lazily and only required when cardSale() is called.

const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });

Methods:

// Charge a tokenized card
const result = await ozura.cardSale({
  token:           tokenResponse.token,
  cvcSession:      tokenResponse.cvcSession,
  amount:          '49.00',
  currency:        'USD',          // default: 'USD'
  billing:         tokenResponse.billing,
  clientIpAddress: '1.2.3.4',     // fetch server-side, never from the browser
  // surchargePercent, tipAmount, salesTaxExempt, processor
});
// result.transactionId, result.amount, result.cardLastFour, result.cardBrand
// result.surchargeAmount and result.tipAmount are optional — only present when non-zero
const surcharge = result.surchargeAmount ?? '0.00';
const tip = result.tipAmount ?? '0.00';

// Create a session key (for custom session endpoint implementations)
const { sessionKey, expiresInSeconds } = await ozura.createSession({
  sessionId,
  sessionLimit: 3,    // must match VaultOptions.sessionLimit on the client (default: 3)
                      // pass null to remove the cap (vault default = unlimited)
  // Optional — stored in vault audit log for correlation with your own records:
  // orderId:    order.id,
  // customerId: user.id,
  // cartId:     cart.id,
  // metadata:   { source: 'web' },
  // ttlSeconds: 600,  // shorter TTL for quicker checkouts (default: 1800)
});

// Revoke a session — call on all three session-end paths
// Best-effort — never throws. Shortens the exposure window before the vault's ~30 min TTL.
await ozura.revokeSession(sessionKey);

// Suggested pattern — wire all three exit paths:
// 1. Payment success
const result = await ozura.cardSale({ ... });
await ozura.revokeSession(sessionKey);   // session is spent; close the window immediately

// 2. User cancels checkout
router.post('/api/cancel', async (req) => {
  const { sessionKey } = await db.session.get(req.sessionId);
  await ozura.revokeSession(sessionKey);
  return Response.json({ ok: true });
});

// 3. Page/tab close (best-effort — browser may not deliver this)
//    Use sendBeacon so the request survives navigation / tab close.
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/api/cancel', JSON.stringify({ sessionId }));
  }
});

// List transactions
const { transactions, pagination } = await ozura.listTransactions({
  dateFrom:        '2025-01-01',
  dateTo:          '2025-12-31',
  transactionType: 'CreditCardSale',
  page:            1,
  limit:           50,
});

OzuraError (thrown by all Ozura methods):

try {
  await ozura.cardSale(input);
} catch (err) {
  if (err instanceof OzuraError) {
    err.statusCode;   // HTTP status code
    err.message;      // Normalized message
    err.raw;          // Raw API response string
    err.retryAfter;   // Seconds (only present on 429)
  }
}

Rate limits: cardSale — 100 req/min per merchant. listTransactions — 200 req/min per merchant.

Retry behavior: createSession and listTransactions retry on 5xx and network errors using exponential backoff (1 s / 2 s / 4 s…) up to retries + 1 total attempts. cardSale is never retried — it is a non-idempotent financial operation and the result of a duplicate charge cannot be predicted.


Route handler factories

The server package exports factory functions covering two runtimes × two endpoints:

| Function | Runtime | Endpoint | |---|---|---| | createSessionHandler | Fetch API (Next.js App Router, Cloudflare, Vercel Edge) | POST /api/oz-session | | createSessionMiddleware | Express / Connect | POST /api/oz-session | | createCardSaleHandler | Fetch API | POST /api/charge | | createCardSaleMiddleware | Express / Connect | POST /api/charge |

createCardSaleHandler / createCardSaleMiddleware accept a CardSaleHandlerOptions object:

interface CardSaleHandlerOptions {
  /**
   * Required. Return the charge amount as a decimal string.
   * Never trust the amount from the request body — resolve it from your database.
   */
  getAmount: (body: Record<string, unknown>) => Promise<string>;

  /**
   * Optional. Return the ISO 4217 currency code. Default: "USD".
   */
  getCurrency?: (body: Record<string, unknown>) => Promise<string>;
}

Both handler factories validate Content-Type: application/json, run validateBilling(), extract the client IP from standard proxy headers, and return normalized { transactionId, amount, cardLastFour, cardBrand } on success.


Local development

The repository includes a development server at dev-server.mjs that serves the built frame assets and proxies vault API requests:

npm run dev   # build + start dev server on http://localhost:4242

Set frameBaseUrl to point your vault at the local server:

const vault = await OzVault.create({
  pubKey: 'pk_test_...',
  sessionUrl: '/api/oz-session',
  frameBaseUrl: 'http://localhost:4242',  // local dev only
});

Or in React:

<OzElements
  pubKey="pk_test_..."
  sessionUrl="/api/oz-session"
  frameBaseUrl="http://localhost:4242"
>

Configure environment variables for the dev server:

VAULT_URL=https://vault-staging.example.com
VAULT_API_KEY=vk_test_...

Content Security Policy

The SDK loads iframes from the Ozura frame origin. Add the following directives to your CSP:

frame-src https://elements.ozura.com;

If loading custom fonts via fonts[].cssSrc, also allow the font stylesheet origin:

style-src  https://fonts.googleapis.com;
font-src   https://fonts.gstatic.com;

To verify your CSP after a build:

npm run check:csp

TypeScript reference

📖 API reference — every interface, union, and enum fully annotated with JSDoc.

All public types are exported from @ozura/elements:

import type {
  // Element types
  ElementType,           // 'cardNumber' | 'cvv' | 'expirationDate'
  BankElementType,       // 'accountNumber' | 'routingNumber'
  ElementOptions,
  ElementStyleConfig,
  ElementStyle,
  ElementChangeEvent,

  // Vault config
  VaultOptions,
  FontSource,
  CssFontSource,
  CustomFontSource,
  Appearance,
  AppearanceVariables,
  OzTheme,               // 'default' | 'night' | 'flat'

  // Tokenization
  TokenizeOptions,
  BankTokenizeOptions,
  TokenResponse,
  BankTokenResponse,
  CardMetadata,
  BankAccountMetadata,

  // Billing
  BillingDetails,
  BillingAddress,

  // Card sale
  CardSaleRequest,
  CardSaleResponseData,
  CardSaleApiResponse,

  // Transactions
  TransactionQueryParams,
  TransactionQueryPagination,
  TransactionQueryResponse,
  TransactionType,
  TransactionData,
  CardTransactionData,
  AchTransactionData,
  CryptoTransactionData,

  // Errors
  OzErrorCode,
} from '@ozura/elements';

Server-specific types are exported from @ozura/elements/server:

import type {
  OzuraConfig,
  CardSaleInput,
  CreateSessionOptions,
  CreateSessionResult,
  ListTransactionsInput,
} from '@ozura/elements/server';

React-specific types are exported from @ozura/elements/react:

import type { OzFieldProps, OzCardProps, OzCardState, OzBankCardProps, OzBankCardState } from '@ozura/elements/react';

Internal documentation

Contributors and maintainers: additional context lives in the untracked docs/ folder (gitignored, local only). This includes architecture notes, security review, audit findings, gap analysis, PCI documentation, and implementation plans. The folder is not shipped in the npm package and never appears in the public repository.


Need help?

The full documentation — including interactive examples, a complete API reference, and integration walkthroughs — lives at:

docs.ozura.com/sdks/elements/overview

| I want to… | Go to | |---|---| | Get started from scratch | Installation | | Build a card payment form | Card elements | | Build an ACH / bank form | Bank elements | | Use the React components | React guide | | Style the input fields | Styling | | Handle errors correctly | Error handling | | Set up the server side | Server SDK | | Look up a type or method | API reference |