@ozura/elements 1.3.0 β†’ 1.3.1-next.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1453 +1,1468 @@
1
- # @ozura/elements
2
-
3
- PCI-isolated card and bank account tokenization SDK for the Ozura Vault.
4
-
5
- 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.
6
-
7
- > **πŸ“– Full documentation:** [docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview)
8
- >
9
- > 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.
10
-
11
- ---
12
-
13
- ## Table of contents
14
-
15
- - [Full documentation](#full-documentation)
16
- - [How it works](#how-it-works)
17
- - [Installation](#installation)
18
- - [Quick start β€” React](#quick-start--react)
19
- - [Quick start β€” Vanilla JS](#quick-start--vanilla-js)
20
- - [Server setup](#server-setup)
21
- - [Session endpoint](#session-endpoint)
22
- - [Card sale endpoint](#card-sale-endpoint)
23
- - [Vanilla JS API](#vanilla-js-api)
24
- - [OzVault.create()](#ozvaultcreate)
25
- - [vault.createElement()](#vaultcreateelementtypeoptions)
26
- - [vault.createToken()](#vaultcreatetokenoptions)
27
- - [vault.createBankElement()](#vaultcreatebankelement)
28
- - [vault.createBankToken()](#vaultcreatebanktokenoptions)
29
- - [vault.reset()](#vaultreset)
30
- - [vault.destroy()](#vaultdestroy)
31
- - [vault.debugState()](#vaultdebugstate)
32
- - [OzElement events](#ozelement-events)
33
- - [React API](#react-api)
34
- - [OzElements provider](#ozelements-provider)
35
- - [OzCard](#ozcard)
36
- - [Individual field components](#individual-field-components)
37
- - [OzBankCard](#ozbankcard)
38
- - [useOzElements()](#useozelements)
39
- - [Styling](#styling)
40
- - [Per-element styles](#per-element-styles)
41
- - [Global appearance](#global-appearance)
42
- - [Custom fonts](#custom-fonts)
43
- - [Billing details](#billing-details)
44
- - [Error handling](#error-handling)
45
- - [Debug mode](#debug-mode)
46
- - [Server utilities](#server-utilities)
47
- - [Ozura class](#ozura-class)
48
- - [Route handler factories](#route-handler-factories)
49
- - [Local development](#local-development)
50
- - [Content Security Policy](#content-security-policy)
51
- - [TypeScript reference](#typescript-reference)
52
-
53
- ---
54
-
55
- ## Full documentation
56
-
57
- The README covers the most common integration patterns. The hosted docs at **[docs.ozura.com/sdks/elements](https://docs.ozura.com/sdks/elements/overview)** go deeper on every topic:
58
-
59
- | Page | URL |
60
- |---|---|
61
- | Overview | [docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview) |
62
- | Installation & setup | [docs.ozura.com/sdks/elements/installation](https://docs.ozura.com/sdks/elements/installation) |
63
- | Card elements | [docs.ozura.com/sdks/elements/card-elements](https://docs.ozura.com/sdks/elements/card-elements) |
64
- | Bank elements | [docs.ozura.com/sdks/elements/bank-elements](https://docs.ozura.com/sdks/elements/bank-elements) |
65
- | React integration | [docs.ozura.com/sdks/elements/react](https://docs.ozura.com/sdks/elements/react) |
66
- | Styling | [docs.ozura.com/sdks/elements/styling](https://docs.ozura.com/sdks/elements/styling) |
67
- | Error handling | [docs.ozura.com/sdks/elements/error-handling](https://docs.ozura.com/sdks/elements/error-handling) |
68
- | Server SDK | [docs.ozura.com/sdks/elements/server](https://docs.ozura.com/sdks/elements/server) |
69
- | API reference | [docs.ozura.com/sdks/elements/api-reference](https://docs.ozura.com/sdks/elements/api-reference) |
70
-
71
- ---
72
-
73
- ## How it works
74
-
75
- ```
76
- Merchant page
77
- β”œβ”€β”€ OzVault (manages tokenizer iframe + element iframes)
78
- β”œβ”€β”€ [hidden] tokenizer-frame.html ← Ozura origin
79
- β”œβ”€β”€ [visible] element-frame.html ← card number ─┐ MessageChannel
80
- β”œβ”€β”€ [visible] element-frame.html ← expiry β”œβ”€ port transfer
81
- └── [visible] element-frame.html ← CVV β”€β”˜
82
- ```
83
-
84
- 1. `OzVault.create()` mounts a hidden tokenizer iframe and fetches a short-lived **session key** from your server.
85
- 2. Calling `vault.createElement()` mounts a visible input iframe for each field.
86
- 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.
87
- 4. The tokenizer POSTs directly to the vault API over HTTPS and returns a token to your page.
88
-
89
- Your server only ever sees a token, never card data.
90
-
91
- ---
92
-
93
- ## Credentials
94
-
95
- | Credential | Format | Where it lives | Required for |
96
- |---|---|---|---|
97
- | **Vault pub key** | `pk_live_…` or `pk_prod_…` | Frontend env var (safe to expose) | All integrations |
98
- | **Vault API key** | `key_…` | Server env var β€” **never in the browser** | Creating sessions (all integrations) |
99
- | **Pay API key** | `ak_…` | Server env var only | OzuraPay merchants (card charging) |
100
- | **Merchant ID** | `ozu_…` | Server env var only | OzuraPay merchants (card charging) |
101
-
102
- If you are not routing payments through OzuraPay you only need the vault pub key (frontend) and vault API key (backend).
103
-
104
- ---
105
-
106
- ## Installation
107
-
108
- > πŸ“– [Installation guide](https://docs.ozura.com/sdks/elements/installation) β€” npm, yarn, CDN setup, and TypeScript configuration.
109
-
110
- ```bash
111
- npm install @ozura/elements
112
- ```
113
-
114
- React and React DOM are peer dependencies (optional β€” only needed for `@ozura/elements/react`):
115
-
116
- ```bash
117
- npm install react react-dom # if not already installed
118
- ```
119
-
120
- **Requirements:** Node β‰₯ 18, React β‰₯ 17 (React peer).
121
-
122
- ---
123
-
124
- ## Quick start β€” React
125
-
126
- > πŸ“– [React integration guide](https://docs.ozura.com/sdks/elements/react) β€” provider setup, pre-built components, hook reference, and full examples.
127
-
128
- ```tsx
129
- // 1. Wrap your checkout in <OzElements>
130
- import { OzElements, OzCard, useOzElements } from '@ozura/elements/react';
131
-
132
- function CheckoutPage() {
133
- return (
134
- <OzElements
135
- pubKey="pk_live_..."
136
- sessionUrl="/api/oz-session"
137
- >
138
- <CheckoutForm />
139
- </OzElements>
140
- );
141
- }
142
-
143
- // 2. Collect card data and tokenize
144
- function CheckoutForm() {
145
- const { createToken, reset, ready } = useOzElements();
146
-
147
- const handleSubmit = async (e: React.FormEvent) => {
148
- e.preventDefault();
149
- try {
150
- const { token, cvcSession, billing } = await createToken({
151
- billing: {
152
- firstName: 'Jane',
153
- lastName: 'Smith',
154
- email: 'jane@example.com',
155
- address: { line1: '123 Main St', city: 'Austin', state: 'TX', zip: '78701', country: 'US' },
156
- },
157
- });
158
-
159
- // Send token to your server
160
- await fetch('/api/charge', {
161
- method: 'POST',
162
- headers: { 'Content-Type': 'application/json' },
163
- body: JSON.stringify({ token, cvcSession, billing }),
164
- });
165
- } catch (err) {
166
- reset(); // clear fields so the customer can re-enter
167
- console.error(err);
168
- }
169
- };
170
-
171
- return (
172
- <form onSubmit={handleSubmit}>
173
- <OzCard onChange={(state) => console.log(state.cardBrand)} />
174
- <button type="submit" disabled={!ready}>Pay</button>
175
- </form>
176
- );
177
- }
178
- ```
179
-
180
- ---
181
-
182
- ## Quick start β€” Vanilla JS
183
-
184
- > πŸ“– [Card elements guide](https://docs.ozura.com/sdks/elements/card-elements) β€” full end-to-end example, field events, submit-button gating, and auto-advance behaviour.
185
-
186
- ```ts
187
- import { OzVault } from '@ozura/elements';
188
-
189
- // Declare state BEFORE OzVault.create(). The onReady callback fires when the
190
- // tokenizer iframe loads β€” this can happen before create() resolves because the
191
- // iframe loads concurrently with fetchWaxKey. At that moment, `vault` is still
192
- // undefined. Do not reference `vault` inside onReady.
193
- let readyCount = 0;
194
- let tokenizerIsReady = false;
195
-
196
- function checkReady() {
197
- if (readyCount === 3 && tokenizerIsReady) enablePayButton();
198
- }
199
-
200
- const vault = await OzVault.create({
201
- pubKey: 'pk_live_...',
202
- sessionUrl: '/api/oz-session',
203
- onReady: () => { tokenizerIsReady = true; checkReady(); }, // tokenizer iframe loaded
204
- });
205
-
206
- const cardNumberEl = vault.createElement('cardNumber');
207
- const expiryEl = vault.createElement('expirationDate');
208
- const cvvEl = vault.createElement('cvv');
209
-
210
- cardNumberEl.mount('#card-number');
211
- expiryEl.mount('#expiry');
212
- cvvEl.mount('#cvv');
213
-
214
- [cardNumberEl, expiryEl, cvvEl].forEach(el => {
215
- el.on('ready', () => { readyCount++; checkReady(); });
216
- });
217
-
218
- async function pay() {
219
- try {
220
- const { token, cvcSession, card } = await vault.createToken({
221
- billing: { firstName: 'Jane', lastName: 'Smith' },
222
- });
223
- // POST { token, cvcSession } to your server
224
- } catch (err) {
225
- vault.reset(); // clear fields; let customer re-enter
226
- console.error(err);
227
- }
228
- }
229
-
230
- // Clean up when done
231
- vault.destroy();
232
- ```
233
-
234
- > **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.
235
-
236
- ---
237
-
238
- ## Server setup
239
-
240
- > πŸ“– [Server SDK guide](https://docs.ozura.com/sdks/elements/server) β€” session creation, card sale handler factories, IP extraction, and manual implementation patterns.
241
-
242
- ### Session endpoint
243
-
244
- 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.
245
-
246
- **Next.js App Router (recommended)**
247
-
248
- ```ts
249
- // app/api/oz-session/route.ts
250
- import { Ozura, createSessionHandler } from '@ozura/elements/server';
251
-
252
- const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
253
-
254
- export const POST = createSessionHandler(ozura);
255
- ```
256
-
257
- **Express**
258
-
259
- ```ts
260
- import express from 'express';
261
- import { Ozura, createSessionMiddleware } from '@ozura/elements/server';
262
-
263
- const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
264
- const app = express();
265
-
266
- app.use(express.json());
267
- app.post('/api/oz-session', createSessionMiddleware(ozura));
268
- ```
269
-
270
- **Manual implementation** (for custom logic or auth checks)
271
-
272
- ```ts
273
- // POST /api/oz-session
274
- const { sessionId } = await req.json();
275
- const { sessionKey } = await ozura.createSession({ sessionId });
276
- return Response.json({ sessionKey });
277
- ```
278
-
279
- ### Card sale endpoint
280
-
281
- After `createToken()` resolves on the frontend, POST `{ token, cvcSession, billing }` to your server to charge the card.
282
-
283
- **Next.js App Router**
284
-
285
- ```ts
286
- // app/api/charge/route.ts
287
- import { Ozura, createCardSaleHandler } from '@ozura/elements/server';
288
-
289
- const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
290
-
291
- export const POST = createCardSaleHandler(ozura, {
292
- getAmount: async (body) => {
293
- const order = await db.orders.findById(body.orderId as string);
294
- return order.total; // decimal string, e.g. "49.00"
295
- },
296
- // getCurrency: async (body) => 'USD', // optional, defaults to "USD"
297
- });
298
- ```
299
-
300
- **Express**
301
-
302
- ```ts
303
- app.post('/api/charge', createCardSaleMiddleware(ozura, {
304
- getAmount: async (body) => {
305
- const order = await db.orders.findById(body.orderId as string);
306
- return order.total; // decimal string, e.g. "49.00"
307
- },
308
- // getCurrency: async (body) => 'USD', // optional, defaults to "USD"
309
- }));
310
- ```
311
-
312
- > `createCardSaleMiddleware` always terminates the request β€” it does not call `next()` and cannot be composed in a middleware chain.
313
-
314
- **Manual implementation**
315
-
316
- ```ts
317
- const { token, cvcSession, billing } = await req.json();
318
- const result = await ozura.cardSale({
319
- token,
320
- cvcSession,
321
- amount: '49.00',
322
- currency: 'USD',
323
- billing,
324
- // Take the first IP from the forwarded-for chain; fall back to socket address.
325
- // req is a Fetch API Request (Next.js App Router / Vercel Edge).
326
- clientIpAddress: req.headers.get('x-forwarded-for')?.split(',')[0].trim()
327
- ?? req.headers.get('x-real-ip')
328
- ?? '',
329
- });
330
- // result.transactionId, result.amount, result.cardLastFour, result.cardBrand
331
- ```
332
-
333
- ---
334
-
335
- ## Vanilla JS API
336
-
337
- > πŸ“– [API reference](https://docs.ozura.com/sdks/elements/api-reference) β€” complete type definitions and method signatures for every class.
338
-
339
- ### OzVault.create(options)
340
-
341
- ```ts
342
- const vault = await OzVault.create(options: VaultOptions): Promise<OzVault>
343
- ```
344
-
345
- 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.
346
-
347
- | Option | Type | Required | Description |
348
- |---|---|---|---|
349
- | `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](https://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. |
350
- | `sessionUrl` | `string` | βœ“ ΒΉ | URL of your session endpoint. The simplest option β€” pass the path and the SDK handles everything. |
351
- | `getSessionKey` | `(sessionId: string) => Promise<string>` | βœ“ ΒΉ | Custom async callback for obtaining the session key. Use when you need custom headers or auth logic. |
352
- | `fetchWaxKey` | `(sessionId: string) => Promise<string>` | βœ“ ΒΉ | **Deprecated.** Use `sessionUrl` or `getSessionKey` instead. |
353
- | `frameBaseUrl` | `string` | β€” | Base URL for iframe assets. Defaults to production CDN. Override for local dev (see [Local development](#local-development)). |
354
- | `fonts` | `FontSource[]` | β€” | Custom fonts to inject into all element iframes. |
355
- | `appearance` | `Appearance` | β€” | Global theme and variable overrides. |
356
- | `loadTimeoutMs` | `number` | β€” | Tokenizer iframe load timeout in ms. Default: `10000`. Only takes effect when `onLoadError` is also provided. |
357
- | `onLoadError` | `() => void` | β€” | Called if the tokenizer iframe fails to load within `loadTimeoutMs`. |
358
- | `onSessionRefresh` | `() => void` | β€” | Called when the SDK silently refreshes the session mid-tokenization (key expired or consumed). |
359
- | `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. |
360
- | `sessionLimit` | `number` | β€” | Card submissions allowed per session before the SDK refreshes automatically. Default: `3`. Must match `sessionLimit` in your server-side `createSession` call. |
361
- | `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](#debug-mode) for details. |
362
-
363
- ΒΉ Exactly one of `sessionUrl`, `getSessionKey`, or `fetchWaxKey` is required.
364
-
365
- Β² `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 `''`.
366
-
367
- Throws `OzError` if the session fetch rejects, returns an empty string, or returns a non-string value.
368
-
369
- > **`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`.
370
-
371
- ---
372
-
373
- ### vault.createElement(type, options?)
374
-
375
- ```ts
376
- vault.createElement(type: ElementType, options?: ElementOptions): OzElement
377
- ```
378
-
379
- Creates and returns an element iframe. Call `.mount(target)` to attach it to the DOM.
380
-
381
- `ElementType`: `'cardNumber'` | `'cvv'` | `'expirationDate'`
382
-
383
- ```ts
384
- const cardEl = vault.createElement('cardNumber', {
385
- placeholder: '1234 5678 9012 3456',
386
- style: {
387
- base: { color: '#1a1a1a', fontSize: '16px' },
388
- focus: { borderColor: '#6366f1' },
389
- invalid: { color: '#dc2626' },
390
- complete: { color: '#16a34a' },
391
- },
392
- });
393
-
394
- cardEl.mount('#card-number-container');
395
- ```
396
-
397
- `ElementOptions`:
398
-
399
- | Option | Type | Description |
400
- |---|---|---|
401
- | `style` | `ElementStyleConfig` | Per-state style overrides. See [Styling](#styling). |
402
- | `placeholder` | `string` | Placeholder text (max 100 characters). |
403
- | `disabled` | `boolean` | Disables the input. |
404
- | `loadTimeoutMs` | `number` | Iframe load timeout in ms. Default: `10000`. |
405
-
406
- **OzElement methods:**
407
-
408
- | Method | Description |
409
- |---|---|
410
- | `.mount(target)` | Mount the iframe. Accepts a CSS selector string or `HTMLElement`. |
411
- | `.unmount()` | Remove the iframe from the DOM. The element can be re-mounted. |
412
- | `.destroy()` | Permanently destroy the element. Cannot be re-mounted. |
413
- | `.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. |
414
- | `.clear()` | Clear the field value. |
415
- | `.focus()` | Programmatically focus the input. |
416
- | `.blur()` | Programmatically blur the input. |
417
- | `.on(event, fn)` | Subscribe to an event. Returns `this` for chaining. |
418
- | `.off(event, fn)` | Remove an event handler. |
419
- | `.once(event, fn)` | Subscribe for a single invocation. |
420
- | `.isReady` | `true` once the iframe has loaded and signalled ready. |
421
-
422
- ---
423
-
424
- ### vault.createToken(options?)
425
-
426
- ```ts
427
- vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>
428
- ```
429
-
430
- 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.
431
-
432
- Returns a `TokenResponse`:
433
-
434
- ```ts
435
- interface TokenResponse {
436
- token: string; // Vault token β€” pass to cardSale
437
- cvcSession: string; // CVC session β€” always present; pass to cardSale
438
- card?: { // Card metadata β€” present when vault returns all four fields
439
- last4: string; // e.g. "4242"
440
- brand: string; // e.g. "visa"
441
- expMonth: string; // e.g. "09"
442
- expYear: string; // e.g. "2027"
443
- };
444
- billing?: BillingDetails; // Normalized billing β€” only present if billing was passed in
445
- }
446
- ```
447
-
448
- `TokenizeOptions`:
449
-
450
- | Option | Type | Description |
451
- |---|---|---|
452
- | `billing` | `BillingDetails` | Validated and normalized billing details. Returned in `TokenResponse.billing`. |
453
- | `firstName` | `string` | **Deprecated.** Pass inside `billing` instead. |
454
- | `lastName` | `string` | **Deprecated.** Pass inside `billing` instead. |
455
-
456
- Throws `OzError` if:
457
- - The vault is not ready (`errorCode: 'unknown'`)
458
- - A tokenization is already in progress
459
- - Billing validation fails (`errorCode: 'validation'`)
460
- - No elements are mounted
461
- - The vault returns an error (`errorCode` reflects the HTTP status)
462
- - The request times out after 30 seconds (`errorCode: 'timeout'`) β€” this timeout is separate from `loadTimeoutMs` and is not configurable
463
-
464
- **`vault.tokenizeCount`**
465
-
466
- ```ts
467
- vault.tokenizeCount: number // read-only getter
468
- ```
469
-
470
- 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:
471
-
472
- ```ts
473
- const MAX = 3; // matches maxTokenizeCalls
474
- const remaining = MAX - vault.tokenizeCount;
475
- payButton.textContent = `Pay (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`;
476
- ```
477
-
478
- In React, use `tokenizeCount` from `useOzElements()` instead β€” it is a reactive state value and will trigger re-renders automatically.
479
-
480
- ---
481
-
482
- ### vault.createBankElement()
483
-
484
- ```ts
485
- vault.createBankElement(type: BankElementType, options?: ElementOptions): OzElement
486
- ```
487
-
488
- Creates a bank account element. `BankElementType`: `'accountNumber'` | `'routingNumber'`.
489
-
490
- ```ts
491
- const accountEl = vault.createBankElement('accountNumber');
492
- const routingEl = vault.createBankElement('routingNumber');
493
- accountEl.mount('#account-number');
494
- routingEl.mount('#routing-number');
495
- ```
496
-
497
- ---
498
-
499
- ### vault.createBankToken(options)
500
-
501
- ```ts
502
- vault.createBankToken(options: BankTokenizeOptions): Promise<BankTokenResponse>
503
- ```
504
-
505
- Tokenizes the mounted `accountNumber` and `routingNumber` elements. Both must be mounted and ready.
506
-
507
- ```ts
508
- interface BankTokenizeOptions {
509
- firstName: string; // Account holder first name (required, max 50 chars)
510
- lastName: string; // Account holder last name (required, max 50 chars)
511
- }
512
-
513
- interface BankTokenResponse {
514
- token: string;
515
- bank?: {
516
- last4: string; // Last 4 digits of account number
517
- routingNumberLast4: string;
518
- };
519
- }
520
- ```
521
-
522
- > **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.
523
-
524
- > **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.
525
-
526
- ---
527
-
528
- ### vault.destroy()
529
-
530
- ```ts
531
- vault.destroy(): void
532
- ```
533
-
534
- 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.
535
-
536
- ```ts
537
- // React useEffect cleanup β€” the cancel flag prevents a vault from leaking
538
- // if the component unmounts before OzVault.create() resolves.
539
- useEffect(() => {
540
- let cancelled = false;
541
- let vault: OzVault | null = null;
542
- OzVault.create(options).then(v => {
543
- if (cancelled) { v.destroy(); return; }
544
- vault = v;
545
- });
546
- return () => {
547
- cancelled = true;
548
- vault?.destroy();
549
- };
550
- }, []);
551
- ```
552
-
553
- ---
554
-
555
- ### vault.reset()
556
-
557
- ```ts
558
- vault.reset(): void
559
- ```
560
-
561
- 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.
562
-
563
- The session key, its remaining budget, and all iframes are fully preserved β€” no network calls are made.
564
-
565
- **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.
566
-
567
- ```ts
568
- try {
569
- const { token, cvcSession } = await vault.createToken({ billing });
570
- await fetch('/api/charge', {
571
- method: 'POST',
572
- headers: { 'Content-Type': 'application/json' },
573
- body: JSON.stringify({ token, cvcSession }),
574
- });
575
- } catch (err) {
576
- vault.reset(); // clear fields; let customer re-enter
577
- showError(err instanceof OzError ? err.message : 'Payment failed.');
578
- }
579
- ```
580
-
581
- ---
582
-
583
- ### vault.debugState()
584
-
585
- ```ts
586
- vault.debugState(): Record<string, unknown>
587
- ```
588
-
589
- 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.
590
-
591
- ```ts
592
- console.log(vault.debugState());
593
- // {
594
- // vaultId: 'vault_abc12...',
595
- // isReady: true,
596
- // tokenizing: null,
597
- // destroyed: false,
598
- // waxKeyPresent: true,
599
- // tokenizeSuccessCount: 1,
600
- // maxTokenizeCalls: 3,
601
- // resetCount: 0,
602
- // elements: ['cardNumber', 'expirationDate', 'cvv'],
603
- // bankElements: [],
604
- // completionState: { 'a1b2c3d4': true, 'e5f6a7b8': true, '...' : false },
605
- // pendingTokenizations: 0,
606
- // pendingBankTokenizations: 0,
607
- // }
608
- ```
609
-
610
- No sensitive data is returned: wax keys, tokens, CVC sessions, and billing fields are never included.
611
-
612
- ---
613
-
614
- ### OzElement events
615
-
616
- ```ts
617
- element.on('change', (event: ElementChangeEvent) => { ... });
618
- element.on('focus', () => { ... });
619
- element.on('blur', () => { ... });
620
- element.on('ready', () => { ... });
621
- element.on('loaderror', (payload: { elementType: string; error: string }) => { ... });
622
- ```
623
-
624
- `ElementChangeEvent`:
625
-
626
- | Field | Type | Description |
627
- |---|---|---|
628
- | `empty` | `boolean` | `true` when the field is empty. |
629
- | `complete` | `boolean` | `true` when the field has enough digits to be complete. |
630
- | `valid` | `boolean` | `true` when the value passes all validation (Luhn, expiry date, etc.). |
631
- | `error` | `string \| undefined` | User-facing error message when `valid` is `false` and the field has been touched. |
632
- | `cardBrand` | `string \| undefined` | Detected brand β€” only on `cardNumber` fields (e.g. `"visa"`, `"amex"`). |
633
- | `month` | `string \| undefined` | Parsed 2-digit month β€” only on `expirationDate` fields. |
634
- | `year` | `string \| undefined` | Parsed 2-digit year β€” only on `expirationDate` fields. |
635
-
636
- > **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.
637
-
638
- Auto-advance is built in: the vault automatically moves focus from card number β†’ expiry β†’ CVV when each field completes. No additional code required.
639
-
640
- ---
641
-
642
- ## React API
643
-
644
- > πŸ“– [React guide](https://docs.ozura.com/sdks/elements/react) β€” `OzElements` provider, `useOzElements` hook, pre-built `OzCard` and `OzBankCard` components, and StrictMode behaviour.
645
-
646
- ### OzElements provider
647
-
648
- ```tsx
649
- import { OzElements } from '@ozura/elements/react';
650
-
651
- <OzElements
652
- pubKey="pk_live_..."
653
- sessionUrl="/api/oz-session"
654
- appearance={{ theme: 'flat', variables: { colorPrimary: '#6366f1' } }}
655
- onLoadError={() => setPaymentUnavailable(true)}
656
- >
657
- {children}
658
- </OzElements>
659
- ```
660
-
661
- All `VaultOptions` are accepted as props. The provider creates a single `OzVault` instance and destroys it on unmount.
662
-
663
- > **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.
664
-
665
- > **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.
666
-
667
- ---
668
-
669
- ### OzCard
670
-
671
- Drop-in combined card component. Renders card number, expiry, and CVV with a configurable layout.
672
-
673
- ```tsx
674
- import { OzCard } from '@ozura/elements/react';
675
-
676
- <OzCard
677
- layout="default" // "default" (number on top, expiry+CVV below) | "rows" (stacked)
678
- onChange={(state) => {
679
- // state.complete β€” all three fields complete + valid
680
- // state.cardBrand β€” detected brand
681
- // state.error β€” first error across all fields
682
- // state.fields β€” per-field ElementChangeEvent objects
683
- }}
684
- onReady={() => console.log('all card fields loaded')}
685
- disabled={isSubmitting}
686
- labels={{ cardNumber: 'Card Number', expiry: 'Expiry', cvv: 'CVV' }}
687
- placeholders={{ cardNumber: '1234 5678 9012 3456', expiry: 'MM/YY', cvv: 'Β·Β·Β·' }}
688
- />
689
- ```
690
-
691
- `OzCardProps` (full):
692
-
693
- | Prop | Type | Description |
694
- |---|---|---|
695
- | `layout` | `'default' \| 'rows'` | `'default'`: number full-width, expiry+CVV side by side. `'rows'`: all stacked. |
696
- | `gap` | `number \| string` | Gap between fields. Default: `8` (px). |
697
- | `style` | `ElementStyleConfig` | Shared style applied to all three inputs. |
698
- | `styles` | `{ cardNumber?, expiry?, cvv? }` | Per-field overrides merged on top of `style`. |
699
- | `classNames` | `{ cardNumber?, expiry?, cvv?, row? }` | CSS class names for field wrappers and the expiry+CVV row. |
700
- | `labels` | `{ cardNumber?, expiry?, cvv? }` | Optional label text above each field. |
701
- | `labelStyle` | `React.CSSProperties` | Style applied to all `<label>` elements. |
702
- | `labelClassName` | `string` | Class applied to all `<label>` elements. |
703
- | `placeholders` | `{ cardNumber?, expiry?, cvv? }` | Custom placeholder text per field. |
704
- | `hideErrors` | `boolean` | Suppress the built-in error display. Handle via `onChange`. |
705
- | `errorStyle` | `React.CSSProperties` | Style for the built-in error container. |
706
- | `errorClassName` | `string` | Class for the built-in error container. |
707
- | `renderError` | `(error: string) => ReactNode` | Custom error renderer. |
708
- | `onChange` | `(state: OzCardState) => void` | Fires on any field change. |
709
- | `onReady` | `() => void` | Fires once all three iframes have loaded. |
710
- | `onFocus` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
711
- | `onBlur` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
712
- | `disabled` | `boolean` | Disable all inputs. |
713
- | `className` | `string` | Class for the outer wrapper. |
714
-
715
- ---
716
-
717
- ### Individual field components
718
-
719
- For custom layouts where `OzCard` is too opinionated:
720
-
721
- ```tsx
722
- import { OzCardNumber, OzExpiry, OzCvv } from '@ozura/elements/react';
723
-
724
- <OzCardNumber onChange={handleChange} placeholder="Card number" />
725
- <OzExpiry onChange={handleChange} />
726
- <OzCvv onChange={handleChange} />
727
- ```
728
-
729
- All accept `OzFieldProps`:
730
-
731
- | Prop | Type | Description |
732
- |---|---|---|
733
- | `style` | `ElementStyleConfig` | Input styles. |
734
- | `placeholder` | `string` | Placeholder text. |
735
- | `disabled` | `boolean` | Disables the input. |
736
- | `loadTimeoutMs` | `number` | Iframe load timeout in ms. |
737
- | `onChange` | `(event: ElementChangeEvent) => void` | |
738
- | `onFocus` | `() => void` | |
739
- | `onBlur` | `() => void` | |
740
- | `onReady` | `() => void` | |
741
- | `onLoadError` | `(error: string) => void` | |
742
- | `className` | `string` | Class for the outer wrapper div. |
743
-
744
- ---
745
-
746
- ### OzBankCard
747
-
748
- ```tsx
749
- import { OzBankCard } from '@ozura/elements/react';
750
-
751
- <OzBankCard
752
- onChange={(state) => {
753
- // state.complete, state.error, state.fields.accountNumber, state.fields.routingNumber
754
- }}
755
- labels={{ accountNumber: 'Account Number', routingNumber: 'Routing Number' }}
756
- />
757
- ```
758
-
759
- Or use individual bank components:
760
-
761
- ```tsx
762
- import { OzBankAccountNumber, OzBankRoutingNumber } from '@ozura/elements/react';
763
-
764
- <OzBankAccountNumber onChange={handleChange} />
765
- <OzBankRoutingNumber onChange={handleChange} />
766
- ```
767
-
768
- ---
769
-
770
- ### useOzElements()
771
-
772
- ```ts
773
- const { createToken, createBankToken, reset, ready, initError, tokenizeCount } = useOzElements();
774
- ```
775
-
776
- Must be called from inside an `<OzElements>` provider tree.
777
-
778
- | Return | Type | Description |
779
- |---|---|---|
780
- | `createToken` | `(options?: TokenizeOptions) => Promise<TokenResponse>` | Tokenize mounted card elements. |
781
- | `createBankToken` | `(options: BankTokenizeOptions) => Promise<BankTokenResponse>` | Tokenize mounted bank elements. |
782
- | `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. |
783
- | `ready` | `boolean` | `true` when the tokenizer **and** all mounted element iframes are ready. Gate your submit button on this. See note below. |
784
- | `initError` | `Error \| null` | Non-null if `OzVault.create()` failed (e.g. session endpoint unreachable). Render a fallback UI. |
785
- | `tokenizeCount` | `number` | Number of successful tokenizations in the current session. Resets on session refresh or provider re-init. Useful for tracking calls against `sessionLimit`. |
786
-
787
- > **`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.
788
-
789
- ---
790
-
791
- ## Vue API
792
-
793
- > πŸ“– 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.
794
-
795
- ### Installation
796
-
797
- npm install @ozura/elements vue
798
-
799
- ### Quick start
800
-
801
- `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:
802
-
803
- **CheckoutPage.vue** β€” renders the provider
804
-
805
- <script setup lang="ts">
806
- import { OzElements } from '@ozura/elements/vue';
807
- import CheckoutForm from './CheckoutForm.vue';
808
- </script>
809
-
810
- <template>
811
- <OzElements pub-key="pk_live_..." session-url="/api/oz-session">
812
- <CheckoutForm />
813
- </OzElements>
814
- </template>
815
-
816
- **CheckoutForm.vue** β€” child component, calls the composable
817
-
818
- <script setup lang="ts">
819
- import { OzCardNumber, OzExpiry, OzCvv, useOzElements } from '@ozura/elements/vue';
820
-
821
- const { createToken, ready } = useOzElements();
822
-
823
- async function handlePay() {
824
- const { token, cvcSession } = await createToken({
825
- billing: { firstName: 'Jane', lastName: 'Doe' },
826
- });
827
- // send token to your backend
828
- }
829
- </script>
830
-
831
- <template>
832
- <OzCardNumber @change="(e) => console.log(e.cardBrand)" />
833
- <OzExpiry />
834
- <OzCvv />
835
- <button :disabled="!ready" @click="handlePay">Pay</button>
836
- </template>
837
-
838
- ### Individual fields
839
-
840
- | Component | Element type |
841
- |---|---|
842
- | `<OzCardNumber>` | Card number |
843
- | `<OzExpiry>` | Expiry date |
844
- | `<OzCvv>` | CVV / CVC |
845
- | `<OzBankAccountNumber>` | Bank account number |
846
- | `<OzBankRoutingNumber>` | Bank routing number |
847
-
848
- ### `<OzElements>` props
849
-
850
- | Prop | Type | Description |
851
- |---|---|---|
852
- | `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. |
853
- | `sessionUrl` | `string` | URL of your backend session endpoint (simplest path). |
854
- | `getSessionKey` | `(sessionId: string) => Promise<string>` | Custom async function for session key. Use instead of `sessionUrl` when you need custom headers or auth. |
855
- | `frameBaseUrl` | `string` | Override the default frame host (`elements.ozura.com`). |
856
- | `fonts` | `FontSource[]` | Custom fonts injected into element iframes. |
857
- | `appearance` | `Appearance` | Global theme and variable overrides applied to all elements. |
858
- | `loadTimeoutMs` | `number` | Iframe load timeout in ms. Default: 10 000. |
859
- | `debug` | `boolean` | Enable structured `[OzVault]` debug logging. Default: `false`. |
860
-
861
- **Event:** `@ready` β€” emitted once when the vault and all mounted field iframes are ready.
862
-
863
- ### `useOzElements()` return values
864
-
865
- | Value | Type | Description |
866
- |---|---|---|
867
- | `createToken` | `(options?: TokenizeOptions) => Promise<TokenResponse>` | Tokenize mounted card fields. |
868
- | `createBankToken` | `(options?: BankTokenizeOptions) => Promise<BankTokenResponse>` | Tokenize mounted bank fields. |
869
- | `ready` | `ComputedRef<boolean>` | `true` when vault and all field iframes are ready. Gate your submit button on this. |
870
- | `initError` | `Ref<Error \| null>` | Non-null if `OzVault.create()` failed. Render a fallback UI. |
871
- | `tokenizeCount` | `Ref<number>` | Successful tokenizations in the current session. Resets on session refresh. |
872
- | `reset` | `() => void` | Clear all fields without destroying the vault. Call after a declined payment. |
873
-
874
- ### Field props and events
875
-
876
- All five field components share the same props and emits:
877
-
878
- **Props:**
879
-
880
- | Prop | Type | Description |
881
- |---|---|---|
882
- | `placeholder` | `string` | Override the default placeholder text. |
883
- | `disabled` | `boolean` | Disable the input. |
884
- | `style` | `ElementStyleConfig` | CSS-in-JS style object applied to the inner input. See [Styling](#styling). |
885
-
886
- **Emits:**
887
-
888
- | Event | Payload | Description |
889
- |---|---|---|
890
- | `@change` | `ElementChangeEvent` | Fires on every keystroke with validation state and metadata. |
891
- | `@focus` | `void` | Fires when the field gains focus. |
892
- | `@blur` | `void` | Fires when the field loses focus. |
893
-
894
- ---
895
-
896
- ## Styling
897
-
898
- > πŸ“– [Styling guide](https://docs.ozura.com/sdks/elements/styling) β€” full property allowlist, theme variables, `appearance` options, custom fonts, and `var()` limitations.
899
-
900
- ### Per-element styles
901
-
902
- 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.
903
-
904
- ```ts
905
- const style: ElementStyleConfig = {
906
- base: {
907
- color: '#1a1a1a',
908
- fontSize: '16px',
909
- fontFamily: '"Inter", sans-serif',
910
- padding: '10px 12px',
911
- backgroundColor: '#ffffff',
912
- borderRadius: '6px',
913
- border: '1px solid #d1d5db',
914
- },
915
- focus: {
916
- borderColor: '#6366f1',
917
- boxShadow: '0 0 0 3px rgba(99,102,241,0.15)',
918
- outline: 'none',
919
- },
920
- invalid: {
921
- borderColor: '#ef4444',
922
- color: '#dc2626',
923
- },
924
- complete: {
925
- borderColor: '#22c55e',
926
- },
927
- placeholder: {
928
- color: '#9ca3af',
929
- },
930
- };
931
- ```
932
-
933
- State precedence: `placeholder` applies to the `::placeholder` pseudo-element. `focus`, `invalid`, and `complete` merge on top of `base`.
934
-
935
- ### Global appearance
936
-
937
- Apply a preset theme and/or variable overrides to all elements at once:
938
-
939
- ```ts
940
- // OzVault.create
941
- const vault = await OzVault.create({
942
- pubKey: '...',
943
- sessionUrl: '/api/oz-session',
944
- appearance: {
945
- theme: 'flat', // 'default' | 'night' | 'flat'
946
- variables: {
947
- colorText: '#1a1a1a',
948
- colorBackground: '#ffffff',
949
- colorPrimary: '#6366f1', // focus caret + color
950
- colorDanger: '#dc2626', // invalid state
951
- colorSuccess: '#16a34a', // complete state
952
- colorPlaceholder: '#9ca3af',
953
- fontFamily: '"Inter", sans-serif',
954
- fontSize: '15px',
955
- fontWeight: '400',
956
- padding: '10px 14px',
957
- letterSpacing: '0.01em',
958
- },
959
- },
960
- });
961
-
962
- // React provider
963
- <OzElements pubKey="..." sessionUrl="..." appearance={{ theme: 'night' }}>
964
- ```
965
-
966
- Per-element `style` takes precedence over `appearance` variables.
967
-
968
- > **`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.
969
- >
970
- > | `appearance` value | Result |
971
- > |---|---|
972
- > | *(omitted entirely)* | No preset β€” element uses minimal built-in defaults |
973
- > | `{}` | Equivalent to `{ theme: 'default' }` β€” full default theme applied |
974
- > | `{ theme: 'night' }` | Night theme |
975
- > | `{ variables: { colorText: '#333' } }` | Default theme + variable overrides |
976
-
977
- ### Custom fonts
978
-
979
- Fonts are injected into each iframe so they render inside the input fields:
980
-
981
- ```ts
982
- fonts: [
983
- // Google Fonts or any HTTPS CSS URL
984
- { cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap' },
985
-
986
- // Custom @font-face
987
- {
988
- family: 'BrandFont',
989
- src: 'url(https://cdn.example.com/brand-font.woff2)',
990
- weight: '400',
991
- style: 'normal',
992
- display: 'swap',
993
- },
994
- ]
995
- ```
996
-
997
- Font `src` values must start with `url(https://...)`. HTTP and data URIs are rejected.
998
-
999
- ---
1000
-
1001
- ## Billing details
1002
-
1003
- ```ts
1004
- interface BillingDetails {
1005
- firstName: string; // 1–50 characters
1006
- lastName: string; // 1–50 characters
1007
- email?: string; // Valid email, max 50 characters
1008
- phone?: string; // E.164 format, e.g. "+15551234567", max 50 characters
1009
- address?: {
1010
- line1: string; // 1–50 characters
1011
- line2?: string; // Optional, omitted from cardSale if blank
1012
- city: string; // 1–50 characters
1013
- state: string; // For US/CA: normalized to 2-letter abbreviation
1014
- zip: string; // 1–50 characters
1015
- country: string; // ISO 3166-1 alpha-2, e.g. "US"
1016
- };
1017
- }
1018
- ```
1019
-
1020
- 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.
1021
-
1022
- ---
1023
-
1024
- ## Error handling
1025
-
1026
- > πŸ“– [Error handling guide](https://docs.ozura.com/sdks/elements/error-handling) β€” `OzError` fields, every `errorCode` value, retry guidance, and session expiry behaviour.
1027
-
1028
- All SDK errors are instances of `OzError`:
1029
-
1030
- ```ts
1031
- import { OzError } from '@ozura/elements';
1032
-
1033
- try {
1034
- const { token } = await vault.createToken({ billing });
1035
- } catch (err) {
1036
- if (err instanceof OzError) {
1037
- switch (err.errorCode) {
1038
- case 'network': // Connection failure β€” show retry UI
1039
- case 'timeout': // 30s deadline exceeded β€” safe to retry
1040
- case 'server': // 5xx from vault β€” transient, safe to retry
1041
- if (err.retryable) showRetryPrompt();
1042
- break;
1043
-
1044
- case 'auth': // Bad pub key / API key β€” configuration issue
1045
- case 'validation': // Bad card data β€” show field-level error
1046
- case 'config': // frameBaseUrl not in permitted allowlist
1047
- case 'unknown':
1048
- showError(err.message);
1049
- break;
1050
- }
1051
- }
1052
- }
1053
- ```
1054
-
1055
- `OzError` fields:
1056
-
1057
- | Field | Type | Description |
1058
- |---|---|---|
1059
- | `message` | `string` | Human-readable, consumer-facing error message. |
1060
- | `errorCode` | `OzErrorCode` | `'network' \| 'timeout' \| 'auth' \| 'validation' \| 'server' \| 'config' \| 'unknown'` |
1061
- | `raw` | `string` | Raw error string from the vault API, if available. |
1062
- | `retryable` | `boolean` | `true` for `network`, `timeout`, `server`. `false` for `auth`, `validation`, `config`, `unknown`. |
1063
-
1064
- > **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.
1065
-
1066
- **Error normalisation helpers** (for displaying errors from `cardSale` to users):
1067
-
1068
- ```ts
1069
- import { normalizeVaultError, normalizeBankVaultError, normalizeCardSaleError } from '@ozura/elements';
1070
-
1071
- // Maps vault tokenize error strings to user-facing copy
1072
- const display = normalizeVaultError(err.raw); // card flows
1073
- const display = normalizeBankVaultError(err.raw); // bank/ACH flows
1074
- const display = normalizeCardSaleError(err.message); // cardSale API errors
1075
- ```
1076
-
1077
- ---
1078
-
1079
- ## Debug mode
1080
-
1081
- Pass `debug: true` in `VaultOptions` (or as a prop on `<OzElements>`) to activate structured console logging at every SDK lifecycle event.
1082
-
1083
- ```ts
1084
- const vault = await OzVault.create({
1085
- pubKey: 'pk_live_...',
1086
- sessionUrl: '/api/oz-session',
1087
- debug: true, // enables [OzVault] console.log output
1088
- });
1089
- ```
1090
-
1091
- ```tsx
1092
- // React
1093
- <OzElements pubKey="pk_live_..." sessionUrl="/api/oz-session" debug>
1094
- ...
1095
- </OzElements>
1096
- ```
1097
-
1098
- Each log entry is a `[OzVault] <message>` prefixed `console.log` call. Events logged include:
1099
-
1100
- | Event | When it fires |
1101
- |---|---|
1102
- | `vault created` | Constructor completes |
1103
- | `wax key received` | `fetchWaxKey` resolves |
1104
- | `mounting tokenizer iframe` | Tokenizer iframe creation begins |
1105
- | `tokenizer iframe ready` | Tokenizer iframe handshake complete |
1106
- | `element iframe ready` | Each card/bank input iframe loads |
1107
- | `field changed` | Per-field `change` event (empty/complete/valid/auto-advance state) |
1108
- | `auto-advance` | Focus moves automatically between card fields |
1109
- | `createToken() called` | Entry to `createToken()` |
1110
- | `OZ_TOKENIZE sent` | Tokenize request dispatched to iframe |
1111
- | `token received` | Token result returned (with elapsed ms) |
1112
- | `token error` | Vault or network error during tokenize |
1113
- | `proactive wax key refresh triggered` | Budget exhausted; refresh starting |
1114
- | `wax key refresh started/succeeded/failed` | Refresh lifecycle |
1115
- | `tab hidden` / `tab visible` | `visibilitychange` events |
1116
- | `reset() called` | `vault.reset()` entry |
1117
- | `destroy() called` | `vault.destroy()` entry |
1118
-
1119
- **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.
1120
-
1121
- ### vault.debugState()
1122
-
1123
- `vault.debugState()` is always available β€” regardless of whether `debug: true` was set β€” and returns a one-time snapshot for attaching to bug reports:
1124
-
1125
- ```ts
1126
- console.log(vault.debugState());
1127
- ```
1128
-
1129
- Sample output:
1130
-
1131
- ```json
1132
- {
1133
- "vaultId": "vault_abc12...",
1134
- "isReady": true,
1135
- "tokenizing": null,
1136
- "destroyed": false,
1137
- "waxKeyPresent": true,
1138
- "tokenizeSuccessCount": 1,
1139
- "maxTokenizeCalls": 3,
1140
- "resetCount": 0,
1141
- "elements": ["cardNumber", "expirationDate", "cvv"],
1142
- "bankElements": [],
1143
- "completionState": { "a1b2c3d4": true, "e5f6a7b8": true, "c9d0e1f2": false },
1144
- "pendingTokenizations": 0,
1145
- "pendingBankTokenizations": 0
1146
- }
1147
- ```
1148
-
1149
- ---
1150
-
1151
- ## Server utilities
1152
-
1153
- > πŸ“– [Server SDK guide](https://docs.ozura.com/sdks/elements/server) β€” `Ozura` class methods, route handler factories, `getClientIp`, error types, and rate limits.
1154
-
1155
- ### Ozura class
1156
-
1157
- ```ts
1158
- import { Ozura, OzuraError } from '@ozura/elements/server';
1159
-
1160
- const ozura = new Ozura({
1161
- merchantId: process.env.MERCHANT_ID!,
1162
- apiKey: process.env.MERCHANT_API_KEY!,
1163
- vaultKey: process.env.VAULT_API_KEY!,
1164
- // apiUrl: 'https://api.ozura.com', // override Pay API URL
1165
- // vaultUrl: 'https://vault.ozura.com', // override vault URL
1166
- timeoutMs: 30000, // default
1167
- retries: 2, // max retry attempts for 5xx/network errors (3 total attempts)
1168
- });
1169
- ```
1170
-
1171
- > **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.
1172
- >
1173
- > ```ts
1174
- > const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
1175
- > ```
1176
-
1177
- **Methods:**
1178
-
1179
- ```ts
1180
- // Charge a tokenized card
1181
- const result = await ozura.cardSale({
1182
- token: tokenResponse.token,
1183
- cvcSession: tokenResponse.cvcSession,
1184
- amount: '49.00',
1185
- currency: 'USD', // default: 'USD'
1186
- billing: tokenResponse.billing,
1187
- clientIpAddress: '1.2.3.4', // fetch server-side, never from the browser
1188
- // surchargePercent, tipAmount, salesTaxExempt, processor
1189
- });
1190
- // result.transactionId, result.amount, result.cardLastFour, result.cardBrand
1191
- // result.surchargeAmount and result.tipAmount are optional β€” only present when non-zero
1192
- const surcharge = result.surchargeAmount ?? '0.00';
1193
- const tip = result.tipAmount ?? '0.00';
1194
-
1195
- // Create a session key (for custom session endpoint implementations)
1196
- const { sessionKey, expiresInSeconds } = await ozura.createSession({
1197
- sessionId,
1198
- sessionLimit: 3, // must match VaultOptions.sessionLimit on the client (default: 3)
1199
- // pass null to remove the cap (vault default = unlimited)
1200
- // Optional β€” stored in vault audit log for correlation with your own records:
1201
- // orderId: order.id,
1202
- // customerId: user.id,
1203
- // cartId: cart.id,
1204
- // metadata: { source: 'web' },
1205
- // ttlSeconds: 600, // shorter TTL for quicker checkouts (default: 1800)
1206
- });
1207
-
1208
- // Revoke a session β€” call on all three session-end paths
1209
- // Best-effort β€” never throws. Shortens the exposure window before the vault's ~30 min TTL.
1210
- await ozura.revokeSession(sessionKey);
1211
-
1212
- // Suggested pattern β€” wire all three exit paths:
1213
- // 1. Payment success
1214
- const result = await ozura.cardSale({ ... });
1215
- await ozura.revokeSession(sessionKey); // session is spent; close the window immediately
1216
-
1217
- // 2. User cancels checkout
1218
- router.post('/api/cancel', async (req) => {
1219
- const { sessionKey } = await db.session.get(req.sessionId);
1220
- await ozura.revokeSession(sessionKey);
1221
- return Response.json({ ok: true });
1222
- });
1223
-
1224
- // 3. Page/tab close (best-effort β€” browser may not deliver this)
1225
- // Use sendBeacon so the request survives navigation / tab close.
1226
- window.addEventListener('visibilitychange', () => {
1227
- if (document.visibilityState === 'hidden') {
1228
- navigator.sendBeacon('/api/cancel', JSON.stringify({ sessionId }));
1229
- }
1230
- });
1231
-
1232
- // List transactions
1233
- const { transactions, pagination } = await ozura.listTransactions({
1234
- dateFrom: '2025-01-01',
1235
- dateTo: '2025-12-31',
1236
- transactionType: 'CreditCardSale',
1237
- page: 1,
1238
- limit: 50,
1239
- });
1240
- ```
1241
-
1242
- **`OzuraError`** (thrown by all `Ozura` methods):
1243
-
1244
- ```ts
1245
- try {
1246
- await ozura.cardSale(input);
1247
- } catch (err) {
1248
- if (err instanceof OzuraError) {
1249
- err.statusCode; // HTTP status code
1250
- err.message; // Normalized message
1251
- err.raw; // Raw API response string
1252
- err.retryAfter; // Seconds (only present on 429)
1253
- }
1254
- }
1255
- ```
1256
-
1257
- Rate limits: `cardSale` β€” 100 req/min per merchant. `listTransactions` β€” 200 req/min per merchant.
1258
-
1259
- > **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.
1260
-
1261
- ---
1262
-
1263
- ### Route handler factories
1264
-
1265
- The server package exports factory functions covering two runtimes Γ— two endpoints:
1266
-
1267
- | Function | Runtime | Endpoint |
1268
- |---|---|---|
1269
- | `createSessionHandler` | Fetch API (Next.js App Router, Cloudflare, Vercel Edge) | `POST /api/oz-session` |
1270
- | `createSessionMiddleware` | Express / Connect | `POST /api/oz-session` |
1271
- | `createCardSaleHandler` | Fetch API | `POST /api/charge` |
1272
- | `createCardSaleMiddleware` | Express / Connect | `POST /api/charge` |
1273
-
1274
- `createCardSaleHandler` / `createCardSaleMiddleware` accept a `CardSaleHandlerOptions` object:
1275
-
1276
- ```ts
1277
- interface CardSaleHandlerOptions {
1278
- /**
1279
- * Required. Return the charge amount as a decimal string.
1280
- * Never trust the amount from the request body β€” resolve it from your database.
1281
- */
1282
- getAmount: (body: Record<string, unknown>) => Promise<string>;
1283
-
1284
- /**
1285
- * Optional. Return the ISO 4217 currency code. Default: "USD".
1286
- */
1287
- getCurrency?: (body: Record<string, unknown>) => Promise<string>;
1288
- }
1289
- ```
1290
-
1291
- 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.
1292
-
1293
- ---
1294
-
1295
- ## Local development
1296
-
1297
- The repository includes a development server at `dev-server.mjs` that serves the built frame assets and proxies vault API requests:
1298
-
1299
- ```bash
1300
- npm run dev # build + start dev server on http://localhost:4242
1301
- ```
1302
-
1303
- Set `frameBaseUrl` to point your vault at the local server:
1304
-
1305
- ```ts
1306
- const vault = await OzVault.create({
1307
- pubKey: 'pk_test_...',
1308
- sessionUrl: '/api/oz-session',
1309
- frameBaseUrl: 'http://localhost:4242', // local dev only
1310
- });
1311
- ```
1312
-
1313
- Or in React:
1314
-
1315
- ```tsx
1316
- <OzElements
1317
- pubKey="pk_test_..."
1318
- sessionUrl="/api/oz-session"
1319
- frameBaseUrl="http://localhost:4242"
1320
- >
1321
- ```
1322
-
1323
- Configure environment variables for the dev server:
1324
-
1325
- ```bash
1326
- VAULT_URL=https://vault-staging.example.com
1327
- VAULT_API_KEY=vk_test_...
1328
- ```
1329
-
1330
- ---
1331
-
1332
- ## Content Security Policy
1333
-
1334
- The SDK loads iframes from the Ozura frame origin. Add the following directives to your CSP:
1335
-
1336
- ```
1337
- frame-src https://elements.ozura.com;
1338
- ```
1339
-
1340
- If loading custom fonts via `fonts[].cssSrc`, also allow the font stylesheet origin:
1341
-
1342
- ```
1343
- style-src https://fonts.googleapis.com;
1344
- font-src https://fonts.gstatic.com;
1345
- ```
1346
-
1347
- To verify your CSP after a build:
1348
-
1349
- ```bash
1350
- npm run check:csp
1351
- ```
1352
-
1353
- ---
1354
-
1355
- ## TypeScript reference
1356
-
1357
- > πŸ“– [API reference](https://docs.ozura.com/sdks/elements/api-reference) β€” every interface, union, and enum fully annotated with JSDoc.
1358
-
1359
- All public types are exported from `@ozura/elements`:
1360
-
1361
- ```ts
1362
- import type {
1363
- // Element types
1364
- ElementType, // 'cardNumber' | 'cvv' | 'expirationDate'
1365
- BankElementType, // 'accountNumber' | 'routingNumber'
1366
- ElementOptions,
1367
- ElementStyleConfig,
1368
- ElementStyle,
1369
- ElementChangeEvent,
1370
-
1371
- // Vault config
1372
- VaultOptions,
1373
- FontSource,
1374
- CssFontSource,
1375
- CustomFontSource,
1376
- Appearance,
1377
- AppearanceVariables,
1378
- OzTheme, // 'default' | 'night' | 'flat'
1379
-
1380
- // Tokenization
1381
- TokenizeOptions,
1382
- BankTokenizeOptions,
1383
- TokenResponse,
1384
- BankTokenResponse,
1385
- CardMetadata,
1386
- BankAccountMetadata,
1387
-
1388
- // Billing
1389
- BillingDetails,
1390
- BillingAddress,
1391
-
1392
- // Card sale
1393
- CardSaleRequest,
1394
- CardSaleResponseData,
1395
- CardSaleApiResponse,
1396
-
1397
- // Transactions
1398
- TransactionQueryParams,
1399
- TransactionQueryPagination,
1400
- TransactionQueryResponse,
1401
- TransactionType,
1402
- TransactionData,
1403
- CardTransactionData,
1404
- AchTransactionData,
1405
- CryptoTransactionData,
1406
-
1407
- // Errors
1408
- OzErrorCode,
1409
- } from '@ozura/elements';
1410
- ```
1411
-
1412
- Server-specific types are exported from `@ozura/elements/server`:
1413
-
1414
- ```ts
1415
- import type {
1416
- OzuraConfig,
1417
- CardSaleInput,
1418
- CreateSessionOptions,
1419
- CreateSessionResult,
1420
- ListTransactionsInput,
1421
- } from '@ozura/elements/server';
1422
- ```
1423
-
1424
- React-specific types are exported from `@ozura/elements/react`:
1425
-
1426
- ```ts
1427
- import type { OzFieldProps, OzCardProps, OzCardState, OzBankCardProps, OzBankCardState } from '@ozura/elements/react';
1428
- ```
1429
-
1430
- ---
1431
-
1432
- ## Internal documentation
1433
-
1434
- 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.
1435
-
1436
- ---
1437
-
1438
- ## Need help?
1439
-
1440
- The full documentation β€” including interactive examples, a complete API reference, and integration walkthroughs β€” lives at:
1441
-
1442
- **[docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview)**
1443
-
1444
- | I want to… | Go to |
1445
- |---|---|
1446
- | Get started from scratch | [Installation](https://docs.ozura.com/sdks/elements/installation) |
1447
- | Build a card payment form | [Card elements](https://docs.ozura.com/sdks/elements/card-elements) |
1448
- | Build an ACH / bank form | [Bank elements](https://docs.ozura.com/sdks/elements/bank-elements) |
1449
- | Use the React components | [React guide](https://docs.ozura.com/sdks/elements/react) |
1450
- | Style the input fields | [Styling](https://docs.ozura.com/sdks/elements/styling) |
1451
- | Handle errors correctly | [Error handling](https://docs.ozura.com/sdks/elements/error-handling) |
1452
- | Set up the server side | [Server SDK](https://docs.ozura.com/sdks/elements/server) |
1453
- | Look up a type or method | [API reference](https://docs.ozura.com/sdks/elements/api-reference) |
1
+ # @ozura/elements
2
+
3
+ PCI-isolated card and bank account tokenization SDK for the Ozura Vault.
4
+
5
+ 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.
6
+
7
+ > **πŸ“– Full documentation:** [docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview)
8
+ >
9
+ > 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.
10
+
11
+ ---
12
+
13
+ ## Table of contents
14
+
15
+ - [Full documentation](#full-documentation)
16
+ - [How it works](#how-it-works)
17
+ - [Installation](#installation)
18
+ - [Quick start β€” React](#quick-start--react)
19
+ - [Quick start β€” Vanilla JS](#quick-start--vanilla-js)
20
+ - [Server setup](#server-setup)
21
+ - [Session endpoint](#session-endpoint)
22
+ - [Card sale endpoint](#card-sale-endpoint)
23
+ - [Vanilla JS API](#vanilla-js-api)
24
+ - [OzVault.create()](#ozvaultcreate)
25
+ - [vault.createElement()](#vaultcreateelementtypeoptions)
26
+ - [vault.createToken()](#vaultcreatetokenoptions)
27
+ - [vault.createBankElement()](#vaultcreatebankelement)
28
+ - [vault.createBankToken()](#vaultcreatebanktokenoptions)
29
+ - [vault.reset()](#vaultreset)
30
+ - [vault.destroy()](#vaultdestroy)
31
+ - [vault.debugState()](#vaultdebugstate)
32
+ - [OzElement events](#ozelement-events)
33
+ - [React API](#react-api)
34
+ - [OzElements provider](#ozelements-provider)
35
+ - [OzCard](#ozcard)
36
+ - [Individual field components](#individual-field-components)
37
+ - [OzBankCard](#ozbankcard)
38
+ - [useOzElements()](#useozelements)
39
+ - [Styling](#styling)
40
+ - [Per-element styles](#per-element-styles)
41
+ - [Global appearance](#global-appearance)
42
+ - [Custom fonts](#custom-fonts)
43
+ - [Billing details](#billing-details)
44
+ - [Error handling](#error-handling)
45
+ - [Debug mode](#debug-mode)
46
+ - [Server utilities](#server-utilities)
47
+ - [Ozura class](#ozura-class)
48
+ - [Route handler factories](#route-handler-factories)
49
+ - [Local development](#local-development)
50
+ - [Content Security Policy](#content-security-policy)
51
+ - [TypeScript reference](#typescript-reference)
52
+
53
+ ---
54
+
55
+ ## Full documentation
56
+
57
+ The README covers the most common integration patterns. The hosted docs at **[docs.ozura.com/sdks/elements](https://docs.ozura.com/sdks/elements/overview)** go deeper on every topic:
58
+
59
+ | Page | URL |
60
+ |---|---|
61
+ | Overview | [docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview) |
62
+ | Installation & setup | [docs.ozura.com/sdks/elements/installation](https://docs.ozura.com/sdks/elements/installation) |
63
+ | Card elements | [docs.ozura.com/sdks/elements/card-elements](https://docs.ozura.com/sdks/elements/card-elements) |
64
+ | Bank elements | [docs.ozura.com/sdks/elements/bank-elements](https://docs.ozura.com/sdks/elements/bank-elements) |
65
+ | React integration | [docs.ozura.com/sdks/elements/react](https://docs.ozura.com/sdks/elements/react) |
66
+ | Styling | [docs.ozura.com/sdks/elements/styling](https://docs.ozura.com/sdks/elements/styling) |
67
+ | Error handling | [docs.ozura.com/sdks/elements/error-handling](https://docs.ozura.com/sdks/elements/error-handling) |
68
+ | Server SDK | [docs.ozura.com/sdks/elements/server](https://docs.ozura.com/sdks/elements/server) |
69
+ | API reference | [docs.ozura.com/sdks/elements/api-reference](https://docs.ozura.com/sdks/elements/api-reference) |
70
+
71
+ ---
72
+
73
+ ## How it works
74
+
75
+ ```
76
+ Merchant page
77
+ β”œβ”€β”€ OzVault (manages tokenizer iframe + element iframes)
78
+ β”œβ”€β”€ [hidden] tokenizer-frame.html ← Ozura origin
79
+ β”œβ”€β”€ [visible] element-frame.html ← card number ─┐ MessageChannel
80
+ β”œβ”€β”€ [visible] element-frame.html ← expiry β”œβ”€ port transfer
81
+ └── [visible] element-frame.html ← CVV β”€β”˜
82
+ ```
83
+
84
+ 1. `OzVault.create()` mounts a hidden tokenizer iframe and fetches a short-lived **session key** from your server.
85
+ 2. Calling `vault.createElement()` mounts a visible input iframe for each field.
86
+ 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.
87
+ 4. The tokenizer POSTs directly to the vault API over HTTPS and returns a token to your page.
88
+
89
+ Your server only ever sees a token, never card data.
90
+
91
+ ---
92
+
93
+ ## Credentials
94
+
95
+ | Credential | Format | Where it lives | Required for |
96
+ |---|---|---|---|
97
+ | **Vault pub key** | `pk_live_…` or `pk_prod_…` | Frontend env var (safe to expose) | All integrations |
98
+ | **Vault API key** | `key_…` | Server env var β€” **never in the browser** | Creating sessions (all integrations) |
99
+ | **Pay API key** | `ak_…` | Server env var only | OzuraPay merchants (card charging) |
100
+ | **Merchant ID** | `ozu_…` | Server env var only | OzuraPay merchants (card charging) |
101
+
102
+ If you are not routing payments through OzuraPay you only need the vault pub key (frontend) and vault API key (backend).
103
+
104
+ ---
105
+
106
+ ## Sandbox and testing
107
+
108
+ To test a full integration end-to-end without processing real money:
109
+
110
+ 1. **Create a vault project** β€” go to [ozuravault.com](https://www.ozuravault.com), sign up, and create a **Test project** (the default for new projects).
111
+ 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.
112
+ 3. **Set up the session endpoint** β€” implement `/api/oz-session` on your backend (see [Server setup](#server-setup)) using your test vault key as `vaultKey`.
113
+ 4. **Add the SDK** β€” install `@ozura/elements` and mount your card fields (see the Quick start sections below).
114
+ 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.
115
+ 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.
116
+
117
+ > **Test card numbers:** See [Test Credentials](https://docs.ozura.com/guides/test-credentials) for card numbers, expected outcomes, and the amount-based result model.
118
+
119
+ ---
120
+
121
+ ## Installation
122
+
123
+ > πŸ“– [Installation guide](https://docs.ozura.com/sdks/elements/installation) β€” npm, yarn, CDN setup, and TypeScript configuration.
124
+
125
+ ```bash
126
+ npm install @ozura/elements
127
+ ```
128
+
129
+ React and React DOM are peer dependencies (optional β€” only needed for `@ozura/elements/react`):
130
+
131
+ ```bash
132
+ npm install react react-dom # if not already installed
133
+ ```
134
+
135
+ **Requirements:** Node β‰₯ 18, React β‰₯ 17 (React peer).
136
+
137
+ ---
138
+
139
+ ## Quick start β€” React
140
+
141
+ > πŸ“– [React integration guide](https://docs.ozura.com/sdks/elements/react) β€” provider setup, pre-built components, hook reference, and full examples.
142
+
143
+ ```tsx
144
+ // 1. Wrap your checkout in <OzElements>
145
+ import { OzElements, OzCard, useOzElements } from '@ozura/elements/react';
146
+
147
+ function CheckoutPage() {
148
+ return (
149
+ <OzElements
150
+ pubKey="pk_live_..."
151
+ sessionUrl="/api/oz-session"
152
+ >
153
+ <CheckoutForm />
154
+ </OzElements>
155
+ );
156
+ }
157
+
158
+ // 2. Collect card data and tokenize
159
+ function CheckoutForm() {
160
+ const { createToken, reset, ready } = useOzElements();
161
+
162
+ const handleSubmit = async (e: React.FormEvent) => {
163
+ e.preventDefault();
164
+ try {
165
+ const { token, cvcSession, billing } = await createToken({
166
+ billing: {
167
+ firstName: 'Jane',
168
+ lastName: 'Smith',
169
+ email: 'jane@example.com',
170
+ address: { line1: '123 Main St', city: 'Austin', state: 'TX', zip: '78701', country: 'US' },
171
+ },
172
+ });
173
+
174
+ // Send token to your server
175
+ await fetch('/api/charge', {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({ token, cvcSession, billing }),
179
+ });
180
+ } catch (err) {
181
+ reset(); // clear fields so the customer can re-enter
182
+ console.error(err);
183
+ }
184
+ };
185
+
186
+ return (
187
+ <form onSubmit={handleSubmit}>
188
+ <OzCard onChange={(state) => console.log(state.cardBrand)} />
189
+ <button type="submit" disabled={!ready}>Pay</button>
190
+ </form>
191
+ );
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Quick start β€” Vanilla JS
198
+
199
+ > πŸ“– [Card elements guide](https://docs.ozura.com/sdks/elements/card-elements) β€” full end-to-end example, field events, submit-button gating, and auto-advance behaviour.
200
+
201
+ ```ts
202
+ import { OzVault } from '@ozura/elements';
203
+
204
+ // Declare state BEFORE OzVault.create(). The onReady callback fires when the
205
+ // tokenizer iframe loads β€” this can happen before create() resolves because the
206
+ // iframe loads concurrently with fetchWaxKey. At that moment, `vault` is still
207
+ // undefined. Do not reference `vault` inside onReady.
208
+ let readyCount = 0;
209
+ let tokenizerIsReady = false;
210
+
211
+ function checkReady() {
212
+ if (readyCount === 3 && tokenizerIsReady) enablePayButton();
213
+ }
214
+
215
+ const vault = await OzVault.create({
216
+ pubKey: 'pk_live_...',
217
+ sessionUrl: '/api/oz-session',
218
+ onReady: () => { tokenizerIsReady = true; checkReady(); }, // tokenizer iframe loaded
219
+ });
220
+
221
+ const cardNumberEl = vault.createElement('cardNumber');
222
+ const expiryEl = vault.createElement('expirationDate');
223
+ const cvvEl = vault.createElement('cvv');
224
+
225
+ cardNumberEl.mount('#card-number');
226
+ expiryEl.mount('#expiry');
227
+ cvvEl.mount('#cvv');
228
+
229
+ [cardNumberEl, expiryEl, cvvEl].forEach(el => {
230
+ el.on('ready', () => { readyCount++; checkReady(); });
231
+ });
232
+
233
+ async function pay() {
234
+ try {
235
+ const { token, cvcSession, card } = await vault.createToken({
236
+ billing: { firstName: 'Jane', lastName: 'Smith' },
237
+ });
238
+ // POST { token, cvcSession } to your server
239
+ } catch (err) {
240
+ vault.reset(); // clear fields; let customer re-enter
241
+ console.error(err);
242
+ }
243
+ }
244
+
245
+ // Clean up when done
246
+ vault.destroy();
247
+ ```
248
+
249
+ > **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.
250
+
251
+ ---
252
+
253
+ ## Server setup
254
+
255
+ > πŸ“– [Server SDK guide](https://docs.ozura.com/sdks/elements/server) β€” session creation, card sale handler factories, IP extraction, and manual implementation patterns.
256
+
257
+ ### Session endpoint
258
+
259
+ 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.
260
+
261
+ **Next.js App Router (recommended)**
262
+
263
+ ```ts
264
+ // app/api/oz-session/route.ts
265
+ import { Ozura, createSessionHandler } from '@ozura/elements/server';
266
+
267
+ const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
268
+
269
+ export const POST = createSessionHandler(ozura);
270
+ ```
271
+
272
+ **Express**
273
+
274
+ ```ts
275
+ import express from 'express';
276
+ import { Ozura, createSessionMiddleware } from '@ozura/elements/server';
277
+
278
+ const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
279
+ const app = express();
280
+
281
+ app.use(express.json());
282
+ app.post('/api/oz-session', createSessionMiddleware(ozura));
283
+ ```
284
+
285
+ **Manual implementation** (for custom logic or auth checks)
286
+
287
+ ```ts
288
+ // POST /api/oz-session
289
+ const { sessionId } = await req.json();
290
+ const { sessionKey } = await ozura.createSession({ sessionId });
291
+ return Response.json({ sessionKey });
292
+ ```
293
+
294
+ ### Card sale endpoint
295
+
296
+ After `createToken()` resolves on the frontend, POST `{ token, cvcSession, billing }` to your server to charge the card.
297
+
298
+ **Next.js App Router**
299
+
300
+ ```ts
301
+ // app/api/charge/route.ts
302
+ import { Ozura, createCardSaleHandler } from '@ozura/elements/server';
303
+
304
+ const ozura = new Ozura({ merchantId: '...', apiKey: '...', vaultKey: '...' });
305
+
306
+ export const POST = createCardSaleHandler(ozura, {
307
+ getAmount: async (body) => {
308
+ const order = await db.orders.findById(body.orderId as string);
309
+ return order.total; // decimal string, e.g. "49.00"
310
+ },
311
+ // getCurrency: async (body) => 'USD', // optional, defaults to "USD"
312
+ });
313
+ ```
314
+
315
+ **Express**
316
+
317
+ ```ts
318
+ app.post('/api/charge', createCardSaleMiddleware(ozura, {
319
+ getAmount: async (body) => {
320
+ const order = await db.orders.findById(body.orderId as string);
321
+ return order.total; // decimal string, e.g. "49.00"
322
+ },
323
+ // getCurrency: async (body) => 'USD', // optional, defaults to "USD"
324
+ }));
325
+ ```
326
+
327
+ > `createCardSaleMiddleware` always terminates the request β€” it does not call `next()` and cannot be composed in a middleware chain.
328
+
329
+ **Manual implementation**
330
+
331
+ ```ts
332
+ const { token, cvcSession, billing } = await req.json();
333
+ const result = await ozura.cardSale({
334
+ token,
335
+ cvcSession,
336
+ amount: '49.00',
337
+ currency: 'USD',
338
+ billing,
339
+ // Take the first IP from the forwarded-for chain; fall back to socket address.
340
+ // req is a Fetch API Request (Next.js App Router / Vercel Edge).
341
+ clientIpAddress: req.headers.get('x-forwarded-for')?.split(',')[0].trim()
342
+ ?? req.headers.get('x-real-ip')
343
+ ?? '',
344
+ });
345
+ // result.transactionId, result.amount, result.cardLastFour, result.cardBrand
346
+ ```
347
+
348
+ ---
349
+
350
+ ## Vanilla JS API
351
+
352
+ > πŸ“– [API reference](https://docs.ozura.com/sdks/elements/api-reference) β€” complete type definitions and method signatures for every class.
353
+
354
+ ### OzVault.create(options)
355
+
356
+ ```ts
357
+ const vault = await OzVault.create(options: VaultOptions): Promise<OzVault>
358
+ ```
359
+
360
+ 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.
361
+
362
+ | Option | Type | Required | Description |
363
+ |---|---|---|---|
364
+ | `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](https://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. |
365
+ | `sessionUrl` | `string` | βœ“ ΒΉ | URL of your session endpoint. The simplest option β€” pass the path and the SDK handles everything. |
366
+ | `getSessionKey` | `(sessionId: string) => Promise<string>` | βœ“ ΒΉ | Custom async callback for obtaining the session key. Use when you need custom headers or auth logic. |
367
+ | `fetchWaxKey` | `(sessionId: string) => Promise<string>` | βœ“ ΒΉ | **Deprecated.** Use `sessionUrl` or `getSessionKey` instead. |
368
+ | `frameBaseUrl` | `string` | β€” | Base URL for iframe assets. Defaults to production CDN. Override for local dev (see [Local development](#local-development)). |
369
+ | `fonts` | `FontSource[]` | β€” | Custom fonts to inject into all element iframes. |
370
+ | `appearance` | `Appearance` | β€” | Global theme and variable overrides. |
371
+ | `loadTimeoutMs` | `number` | β€” | Tokenizer iframe load timeout in ms. Default: `10000`. Only takes effect when `onLoadError` is also provided. |
372
+ | `onLoadError` | `() => void` | β€” | Called if the tokenizer iframe fails to load within `loadTimeoutMs`. |
373
+ | `onSessionRefresh` | `() => void` | β€” | Called when the SDK silently refreshes the session mid-tokenization (key expired or consumed). |
374
+ | `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. |
375
+ | `sessionLimit` | `number` | β€” | Card submissions allowed per session before the SDK refreshes automatically. Default: `3`. Must match `sessionLimit` in your server-side `createSession` call. |
376
+ | `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](#debug-mode) for details. |
377
+
378
+ ΒΉ Exactly one of `sessionUrl`, `getSessionKey`, or `fetchWaxKey` is required.
379
+
380
+ Β² `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 `''`.
381
+
382
+ Throws `OzError` if the session fetch rejects, returns an empty string, or returns a non-string value.
383
+
384
+ > **`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`.
385
+
386
+ ---
387
+
388
+ ### vault.createElement(type, options?)
389
+
390
+ ```ts
391
+ vault.createElement(type: ElementType, options?: ElementOptions): OzElement
392
+ ```
393
+
394
+ Creates and returns an element iframe. Call `.mount(target)` to attach it to the DOM.
395
+
396
+ `ElementType`: `'cardNumber'` | `'cvv'` | `'expirationDate'`
397
+
398
+ ```ts
399
+ const cardEl = vault.createElement('cardNumber', {
400
+ placeholder: '1234 5678 9012 3456',
401
+ style: {
402
+ base: { color: '#1a1a1a', fontSize: '16px' },
403
+ focus: { borderColor: '#6366f1' },
404
+ invalid: { color: '#dc2626' },
405
+ complete: { color: '#16a34a' },
406
+ },
407
+ });
408
+
409
+ cardEl.mount('#card-number-container');
410
+ ```
411
+
412
+ `ElementOptions`:
413
+
414
+ | Option | Type | Description |
415
+ |---|---|---|
416
+ | `style` | `ElementStyleConfig` | Per-state style overrides. See [Styling](#styling). |
417
+ | `placeholder` | `string` | Placeholder text (max 100 characters). |
418
+ | `disabled` | `boolean` | Disables the input. |
419
+ | `loadTimeoutMs` | `number` | Iframe load timeout in ms. Default: `10000`. |
420
+
421
+ **OzElement methods:**
422
+
423
+ | Method | Description |
424
+ |---|---|
425
+ | `.mount(target)` | Mount the iframe. Accepts a CSS selector string or `HTMLElement`. |
426
+ | `.unmount()` | Remove the iframe from the DOM. The element can be re-mounted. |
427
+ | `.destroy()` | Permanently destroy the element. Cannot be re-mounted. |
428
+ | `.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. |
429
+ | `.clear()` | Clear the field value. |
430
+ | `.focus()` | Programmatically focus the input. |
431
+ | `.blur()` | Programmatically blur the input. |
432
+ | `.on(event, fn)` | Subscribe to an event. Returns `this` for chaining. |
433
+ | `.off(event, fn)` | Remove an event handler. |
434
+ | `.once(event, fn)` | Subscribe for a single invocation. |
435
+ | `.isReady` | `true` once the iframe has loaded and signalled ready. |
436
+
437
+ ---
438
+
439
+ ### vault.createToken(options?)
440
+
441
+ ```ts
442
+ vault.createToken(options?: TokenizeOptions): Promise<TokenResponse>
443
+ ```
444
+
445
+ 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.
446
+
447
+ Returns a `TokenResponse`:
448
+
449
+ ```ts
450
+ interface TokenResponse {
451
+ token: string; // Vault token β€” pass to cardSale
452
+ cvcSession: string; // CVC session β€” always present; pass to cardSale
453
+ card?: { // Card metadata β€” present when vault returns all four fields
454
+ last4: string; // e.g. "4242"
455
+ brand: string; // e.g. "visa"
456
+ expMonth: string; // e.g. "09"
457
+ expYear: string; // e.g. "2027"
458
+ };
459
+ billing?: BillingDetails; // Normalized billing β€” only present if billing was passed in
460
+ }
461
+ ```
462
+
463
+ `TokenizeOptions`:
464
+
465
+ | Option | Type | Description |
466
+ |---|---|---|
467
+ | `billing` | `BillingDetails` | Validated and normalized billing details. Returned in `TokenResponse.billing`. |
468
+ | `firstName` | `string` | **Deprecated.** Pass inside `billing` instead. |
469
+ | `lastName` | `string` | **Deprecated.** Pass inside `billing` instead. |
470
+
471
+ Throws `OzError` if:
472
+ - The vault is not ready (`errorCode: 'unknown'`)
473
+ - A tokenization is already in progress
474
+ - Billing validation fails (`errorCode: 'validation'`)
475
+ - No elements are mounted
476
+ - The vault returns an error (`errorCode` reflects the HTTP status)
477
+ - The request times out after 30 seconds (`errorCode: 'timeout'`) β€” this timeout is separate from `loadTimeoutMs` and is not configurable
478
+
479
+ **`vault.tokenizeCount`**
480
+
481
+ ```ts
482
+ vault.tokenizeCount: number // read-only getter
483
+ ```
484
+
485
+ 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:
486
+
487
+ ```ts
488
+ const MAX = 3; // matches maxTokenizeCalls
489
+ const remaining = MAX - vault.tokenizeCount;
490
+ payButton.textContent = `Pay (${remaining} attempt${remaining === 1 ? '' : 's'} remaining)`;
491
+ ```
492
+
493
+ In React, use `tokenizeCount` from `useOzElements()` instead β€” it is a reactive state value and will trigger re-renders automatically.
494
+
495
+ ---
496
+
497
+ ### vault.createBankElement()
498
+
499
+ ```ts
500
+ vault.createBankElement(type: BankElementType, options?: ElementOptions): OzElement
501
+ ```
502
+
503
+ Creates a bank account element. `BankElementType`: `'accountNumber'` | `'routingNumber'`.
504
+
505
+ ```ts
506
+ const accountEl = vault.createBankElement('accountNumber');
507
+ const routingEl = vault.createBankElement('routingNumber');
508
+ accountEl.mount('#account-number');
509
+ routingEl.mount('#routing-number');
510
+ ```
511
+
512
+ ---
513
+
514
+ ### vault.createBankToken(options)
515
+
516
+ ```ts
517
+ vault.createBankToken(options: BankTokenizeOptions): Promise<BankTokenResponse>
518
+ ```
519
+
520
+ Tokenizes the mounted `accountNumber` and `routingNumber` elements. Both must be mounted and ready.
521
+
522
+ ```ts
523
+ interface BankTokenizeOptions {
524
+ firstName: string; // Account holder first name (required, max 50 chars)
525
+ lastName: string; // Account holder last name (required, max 50 chars)
526
+ }
527
+
528
+ interface BankTokenResponse {
529
+ token: string;
530
+ bank?: {
531
+ last4: string; // Last 4 digits of account number
532
+ routingNumberLast4: string;
533
+ };
534
+ }
535
+ ```
536
+
537
+ > **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.
538
+
539
+ > **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.
540
+
541
+ ---
542
+
543
+ ### vault.destroy()
544
+
545
+ ```ts
546
+ vault.destroy(): void
547
+ ```
548
+
549
+ 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.
550
+
551
+ ```ts
552
+ // React useEffect cleanup β€” the cancel flag prevents a vault from leaking
553
+ // if the component unmounts before OzVault.create() resolves.
554
+ useEffect(() => {
555
+ let cancelled = false;
556
+ let vault: OzVault | null = null;
557
+ OzVault.create(options).then(v => {
558
+ if (cancelled) { v.destroy(); return; }
559
+ vault = v;
560
+ });
561
+ return () => {
562
+ cancelled = true;
563
+ vault?.destroy();
564
+ };
565
+ }, []);
566
+ ```
567
+
568
+ ---
569
+
570
+ ### vault.reset()
571
+
572
+ ```ts
573
+ vault.reset(): void
574
+ ```
575
+
576
+ 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.
577
+
578
+ The session key, its remaining budget, and all iframes are fully preserved β€” no network calls are made.
579
+
580
+ **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.
581
+
582
+ ```ts
583
+ try {
584
+ const { token, cvcSession } = await vault.createToken({ billing });
585
+ await fetch('/api/charge', {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json' },
588
+ body: JSON.stringify({ token, cvcSession }),
589
+ });
590
+ } catch (err) {
591
+ vault.reset(); // clear fields; let customer re-enter
592
+ showError(err instanceof OzError ? err.message : 'Payment failed.');
593
+ }
594
+ ```
595
+
596
+ ---
597
+
598
+ ### vault.debugState()
599
+
600
+ ```ts
601
+ vault.debugState(): Record<string, unknown>
602
+ ```
603
+
604
+ 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.
605
+
606
+ ```ts
607
+ console.log(vault.debugState());
608
+ // {
609
+ // vaultId: 'vault_abc12...',
610
+ // isReady: true,
611
+ // tokenizing: null,
612
+ // destroyed: false,
613
+ // waxKeyPresent: true,
614
+ // tokenizeSuccessCount: 1,
615
+ // maxTokenizeCalls: 3,
616
+ // resetCount: 0,
617
+ // elements: ['cardNumber', 'expirationDate', 'cvv'],
618
+ // bankElements: [],
619
+ // completionState: { 'a1b2c3d4': true, 'e5f6a7b8': true, '...' : false },
620
+ // pendingTokenizations: 0,
621
+ // pendingBankTokenizations: 0,
622
+ // }
623
+ ```
624
+
625
+ No sensitive data is returned: wax keys, tokens, CVC sessions, and billing fields are never included.
626
+
627
+ ---
628
+
629
+ ### OzElement events
630
+
631
+ ```ts
632
+ element.on('change', (event: ElementChangeEvent) => { ... });
633
+ element.on('focus', () => { ... });
634
+ element.on('blur', () => { ... });
635
+ element.on('ready', () => { ... });
636
+ element.on('loaderror', (payload: { elementType: string; error: string }) => { ... });
637
+ ```
638
+
639
+ `ElementChangeEvent`:
640
+
641
+ | Field | Type | Description |
642
+ |---|---|---|
643
+ | `empty` | `boolean` | `true` when the field is empty. |
644
+ | `complete` | `boolean` | `true` when the field has enough digits to be complete. |
645
+ | `valid` | `boolean` | `true` when the value passes all validation (Luhn, expiry date, etc.). |
646
+ | `error` | `string \| undefined` | User-facing error message when `valid` is `false` and the field has been touched. |
647
+ | `cardBrand` | `string \| undefined` | Detected brand β€” only on `cardNumber` fields (e.g. `"visa"`, `"amex"`). |
648
+ | `month` | `string \| undefined` | Parsed 2-digit month β€” only on `expirationDate` fields. |
649
+ | `year` | `string \| undefined` | Parsed 2-digit year β€” only on `expirationDate` fields. |
650
+
651
+ > **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.
652
+
653
+ Auto-advance is built in: the vault automatically moves focus from card number β†’ expiry β†’ CVV when each field completes. No additional code required.
654
+
655
+ ---
656
+
657
+ ## React API
658
+
659
+ > πŸ“– [React guide](https://docs.ozura.com/sdks/elements/react) β€” `OzElements` provider, `useOzElements` hook, pre-built `OzCard` and `OzBankCard` components, and StrictMode behaviour.
660
+
661
+ ### OzElements provider
662
+
663
+ ```tsx
664
+ import { OzElements } from '@ozura/elements/react';
665
+
666
+ <OzElements
667
+ pubKey="pk_live_..."
668
+ sessionUrl="/api/oz-session"
669
+ appearance={{ theme: 'flat', variables: { colorPrimary: '#6366f1' } }}
670
+ onLoadError={() => setPaymentUnavailable(true)}
671
+ >
672
+ {children}
673
+ </OzElements>
674
+ ```
675
+
676
+ All `VaultOptions` are accepted as props. The provider creates a single `OzVault` instance and destroys it on unmount.
677
+
678
+ > **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.
679
+
680
+ > **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.
681
+
682
+ ---
683
+
684
+ ### OzCard
685
+
686
+ Drop-in combined card component. Renders card number, expiry, and CVV with a configurable layout.
687
+
688
+ ```tsx
689
+ import { OzCard } from '@ozura/elements/react';
690
+
691
+ <OzCard
692
+ layout="default" // "default" (number on top, expiry+CVV below) | "rows" (stacked)
693
+ onChange={(state) => {
694
+ // state.complete β€” all three fields complete + valid
695
+ // state.cardBrand β€” detected brand
696
+ // state.error β€” first error across all fields
697
+ // state.fields β€” per-field ElementChangeEvent objects
698
+ }}
699
+ onReady={() => console.log('all card fields loaded')}
700
+ disabled={isSubmitting}
701
+ labels={{ cardNumber: 'Card Number', expiry: 'Expiry', cvv: 'CVV' }}
702
+ placeholders={{ cardNumber: '1234 5678 9012 3456', expiry: 'MM/YY', cvv: 'Β·Β·Β·' }}
703
+ />
704
+ ```
705
+
706
+ `OzCardProps` (full):
707
+
708
+ | Prop | Type | Description |
709
+ |---|---|---|
710
+ | `layout` | `'default' \| 'rows'` | `'default'`: number full-width, expiry+CVV side by side. `'rows'`: all stacked. |
711
+ | `gap` | `number \| string` | Gap between fields. Default: `8` (px). |
712
+ | `style` | `ElementStyleConfig` | Shared style applied to all three inputs. |
713
+ | `styles` | `{ cardNumber?, expiry?, cvv? }` | Per-field overrides merged on top of `style`. |
714
+ | `classNames` | `{ cardNumber?, expiry?, cvv?, row? }` | CSS class names for field wrappers and the expiry+CVV row. |
715
+ | `labels` | `{ cardNumber?, expiry?, cvv? }` | Optional label text above each field. |
716
+ | `labelStyle` | `React.CSSProperties` | Style applied to all `<label>` elements. |
717
+ | `labelClassName` | `string` | Class applied to all `<label>` elements. |
718
+ | `placeholders` | `{ cardNumber?, expiry?, cvv? }` | Custom placeholder text per field. |
719
+ | `hideErrors` | `boolean` | Suppress the built-in error display. Handle via `onChange`. |
720
+ | `errorStyle` | `React.CSSProperties` | Style for the built-in error container. |
721
+ | `errorClassName` | `string` | Class for the built-in error container. |
722
+ | `renderError` | `(error: string) => ReactNode` | Custom error renderer. |
723
+ | `onChange` | `(state: OzCardState) => void` | Fires on any field change. |
724
+ | `onReady` | `() => void` | Fires once all three iframes have loaded. |
725
+ | `onFocus` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
726
+ | `onBlur` | `(field: 'cardNumber' \| 'expiry' \| 'cvv') => void` | |
727
+ | `disabled` | `boolean` | Disable all inputs. |
728
+ | `className` | `string` | Class for the outer wrapper. |
729
+
730
+ ---
731
+
732
+ ### Individual field components
733
+
734
+ For custom layouts where `OzCard` is too opinionated:
735
+
736
+ ```tsx
737
+ import { OzCardNumber, OzExpiry, OzCvv } from '@ozura/elements/react';
738
+
739
+ <OzCardNumber onChange={handleChange} placeholder="Card number" />
740
+ <OzExpiry onChange={handleChange} />
741
+ <OzCvv onChange={handleChange} />
742
+ ```
743
+
744
+ All accept `OzFieldProps`:
745
+
746
+ | Prop | Type | Description |
747
+ |---|---|---|
748
+ | `style` | `ElementStyleConfig` | Input styles. |
749
+ | `placeholder` | `string` | Placeholder text. |
750
+ | `disabled` | `boolean` | Disables the input. |
751
+ | `loadTimeoutMs` | `number` | Iframe load timeout in ms. |
752
+ | `onChange` | `(event: ElementChangeEvent) => void` | |
753
+ | `onFocus` | `() => void` | |
754
+ | `onBlur` | `() => void` | |
755
+ | `onReady` | `() => void` | |
756
+ | `onLoadError` | `(error: string) => void` | |
757
+ | `className` | `string` | Class for the outer wrapper div. |
758
+
759
+ ---
760
+
761
+ ### OzBankCard
762
+
763
+ ```tsx
764
+ import { OzBankCard } from '@ozura/elements/react';
765
+
766
+ <OzBankCard
767
+ onChange={(state) => {
768
+ // state.complete, state.error, state.fields.accountNumber, state.fields.routingNumber
769
+ }}
770
+ labels={{ accountNumber: 'Account Number', routingNumber: 'Routing Number' }}
771
+ />
772
+ ```
773
+
774
+ Or use individual bank components:
775
+
776
+ ```tsx
777
+ import { OzBankAccountNumber, OzBankRoutingNumber } from '@ozura/elements/react';
778
+
779
+ <OzBankAccountNumber onChange={handleChange} />
780
+ <OzBankRoutingNumber onChange={handleChange} />
781
+ ```
782
+
783
+ ---
784
+
785
+ ### useOzElements()
786
+
787
+ ```ts
788
+ const { createToken, createBankToken, reset, ready, initError, tokenizeCount } = useOzElements();
789
+ ```
790
+
791
+ Must be called from inside an `<OzElements>` provider tree.
792
+
793
+ | Return | Type | Description |
794
+ |---|---|---|
795
+ | `createToken` | `(options?: TokenizeOptions) => Promise<TokenResponse>` | Tokenize mounted card elements. |
796
+ | `createBankToken` | `(options: BankTokenizeOptions) => Promise<BankTokenResponse>` | Tokenize mounted bank elements. |
797
+ | `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. |
798
+ | `ready` | `boolean` | `true` when the tokenizer **and** all mounted element iframes are ready. Gate your submit button on this. See note below. |
799
+ | `initError` | `Error \| null` | Non-null if `OzVault.create()` failed (e.g. session endpoint unreachable). Render a fallback UI. |
800
+ | `tokenizeCount` | `number` | Number of successful tokenizations in the current session. Resets on session refresh or provider re-init. Useful for tracking calls against `sessionLimit`. |
801
+
802
+ > **`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.
803
+
804
+ ---
805
+
806
+ ## Vue API
807
+
808
+ > πŸ“– 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.
809
+
810
+ ### Installation
811
+
812
+ npm install @ozura/elements vue
813
+
814
+ ### Quick start
815
+
816
+ `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:
817
+
818
+ **CheckoutPage.vue** β€” renders the provider
819
+
820
+ <script setup lang="ts">
821
+ import { OzElements } from '@ozura/elements/vue';
822
+ import CheckoutForm from './CheckoutForm.vue';
823
+ </script>
824
+
825
+ <template>
826
+ <OzElements pub-key="pk_live_..." session-url="/api/oz-session">
827
+ <CheckoutForm />
828
+ </OzElements>
829
+ </template>
830
+
831
+ **CheckoutForm.vue** β€” child component, calls the composable
832
+
833
+ <script setup lang="ts">
834
+ import { OzCardNumber, OzExpiry, OzCvv, useOzElements } from '@ozura/elements/vue';
835
+
836
+ const { createToken, ready } = useOzElements();
837
+
838
+ async function handlePay() {
839
+ const { token, cvcSession } = await createToken({
840
+ billing: { firstName: 'Jane', lastName: 'Doe' },
841
+ });
842
+ // send token to your backend
843
+ }
844
+ </script>
845
+
846
+ <template>
847
+ <OzCardNumber @change="(e) => console.log(e.cardBrand)" />
848
+ <OzExpiry />
849
+ <OzCvv />
850
+ <button :disabled="!ready" @click="handlePay">Pay</button>
851
+ </template>
852
+
853
+ ### Individual fields
854
+
855
+ | Component | Element type |
856
+ |---|---|
857
+ | `<OzCardNumber>` | Card number |
858
+ | `<OzExpiry>` | Expiry date |
859
+ | `<OzCvv>` | CVV / CVC |
860
+ | `<OzBankAccountNumber>` | Bank account number |
861
+ | `<OzBankRoutingNumber>` | Bank routing number |
862
+
863
+ ### `<OzElements>` props
864
+
865
+ | Prop | Type | Description |
866
+ |---|---|---|
867
+ | `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. |
868
+ | `sessionUrl` | `string` | URL of your backend session endpoint (simplest path). |
869
+ | `getSessionKey` | `(sessionId: string) => Promise<string>` | Custom async function for session key. Use instead of `sessionUrl` when you need custom headers or auth. |
870
+ | `frameBaseUrl` | `string` | Override the default frame host (`elements.ozura.com`). |
871
+ | `fonts` | `FontSource[]` | Custom fonts injected into element iframes. |
872
+ | `appearance` | `Appearance` | Global theme and variable overrides applied to all elements. |
873
+ | `loadTimeoutMs` | `number` | Iframe load timeout in ms. Default: 10 000. |
874
+ | `debug` | `boolean` | Enable structured `[OzVault]` debug logging. Default: `false`. |
875
+
876
+ **Event:** `@ready` β€” emitted once when the vault and all mounted field iframes are ready.
877
+
878
+ ### `useOzElements()` return values
879
+
880
+ | Value | Type | Description |
881
+ |---|---|---|
882
+ | `createToken` | `(options?: TokenizeOptions) => Promise<TokenResponse>` | Tokenize mounted card fields. |
883
+ | `createBankToken` | `(options?: BankTokenizeOptions) => Promise<BankTokenResponse>` | Tokenize mounted bank fields. |
884
+ | `ready` | `ComputedRef<boolean>` | `true` when vault and all field iframes are ready. Gate your submit button on this. |
885
+ | `initError` | `Ref<Error \| null>` | Non-null if `OzVault.create()` failed. Render a fallback UI. |
886
+ | `tokenizeCount` | `Ref<number>` | Successful tokenizations in the current session. Resets on session refresh. |
887
+ | `reset` | `() => void` | Clear all fields without destroying the vault. Call after a declined payment. |
888
+
889
+ ### Field props and events
890
+
891
+ All five field components share the same props and emits:
892
+
893
+ **Props:**
894
+
895
+ | Prop | Type | Description |
896
+ |---|---|---|
897
+ | `placeholder` | `string` | Override the default placeholder text. |
898
+ | `disabled` | `boolean` | Disable the input. |
899
+ | `style` | `ElementStyleConfig` | CSS-in-JS style object applied to the inner input. See [Styling](#styling). |
900
+
901
+ **Emits:**
902
+
903
+ | Event | Payload | Description |
904
+ |---|---|---|
905
+ | `@change` | `ElementChangeEvent` | Fires on every keystroke with validation state and metadata. |
906
+ | `@focus` | `void` | Fires when the field gains focus. |
907
+ | `@blur` | `void` | Fires when the field loses focus. |
908
+
909
+ ---
910
+
911
+ ## Styling
912
+
913
+ > πŸ“– [Styling guide](https://docs.ozura.com/sdks/elements/styling) β€” full property allowlist, theme variables, `appearance` options, custom fonts, and `var()` limitations.
914
+
915
+ ### Per-element styles
916
+
917
+ 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.
918
+
919
+ ```ts
920
+ const style: ElementStyleConfig = {
921
+ base: {
922
+ color: '#1a1a1a',
923
+ fontSize: '16px',
924
+ fontFamily: '"Inter", sans-serif',
925
+ padding: '10px 12px',
926
+ backgroundColor: '#ffffff',
927
+ borderRadius: '6px',
928
+ border: '1px solid #d1d5db',
929
+ },
930
+ focus: {
931
+ borderColor: '#6366f1',
932
+ boxShadow: '0 0 0 3px rgba(99,102,241,0.15)',
933
+ outline: 'none',
934
+ },
935
+ invalid: {
936
+ borderColor: '#ef4444',
937
+ color: '#dc2626',
938
+ },
939
+ complete: {
940
+ borderColor: '#22c55e',
941
+ },
942
+ placeholder: {
943
+ color: '#9ca3af',
944
+ },
945
+ };
946
+ ```
947
+
948
+ State precedence: `placeholder` applies to the `::placeholder` pseudo-element. `focus`, `invalid`, and `complete` merge on top of `base`.
949
+
950
+ ### Global appearance
951
+
952
+ Apply a preset theme and/or variable overrides to all elements at once:
953
+
954
+ ```ts
955
+ // OzVault.create
956
+ const vault = await OzVault.create({
957
+ pubKey: '...',
958
+ sessionUrl: '/api/oz-session',
959
+ appearance: {
960
+ theme: 'flat', // 'default' | 'night' | 'flat'
961
+ variables: {
962
+ colorText: '#1a1a1a',
963
+ colorBackground: '#ffffff',
964
+ colorPrimary: '#6366f1', // focus caret + color
965
+ colorDanger: '#dc2626', // invalid state
966
+ colorSuccess: '#16a34a', // complete state
967
+ colorPlaceholder: '#9ca3af',
968
+ fontFamily: '"Inter", sans-serif',
969
+ fontSize: '15px',
970
+ fontWeight: '400',
971
+ padding: '10px 14px',
972
+ letterSpacing: '0.01em',
973
+ },
974
+ },
975
+ });
976
+
977
+ // React provider
978
+ <OzElements pubKey="..." sessionUrl="..." appearance={{ theme: 'night' }}>
979
+ ```
980
+
981
+ Per-element `style` takes precedence over `appearance` variables.
982
+
983
+ > **`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.
984
+ >
985
+ > | `appearance` value | Result |
986
+ > |---|---|
987
+ > | *(omitted entirely)* | No preset β€” element uses minimal built-in defaults |
988
+ > | `{}` | Equivalent to `{ theme: 'default' }` β€” full default theme applied |
989
+ > | `{ theme: 'night' }` | Night theme |
990
+ > | `{ variables: { colorText: '#333' } }` | Default theme + variable overrides |
991
+
992
+ ### Custom fonts
993
+
994
+ Fonts are injected into each iframe so they render inside the input fields:
995
+
996
+ ```ts
997
+ fonts: [
998
+ // Google Fonts or any HTTPS CSS URL
999
+ { cssSrc: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap' },
1000
+
1001
+ // Custom @font-face
1002
+ {
1003
+ family: 'BrandFont',
1004
+ src: 'url(https://cdn.example.com/brand-font.woff2)',
1005
+ weight: '400',
1006
+ style: 'normal',
1007
+ display: 'swap',
1008
+ },
1009
+ ]
1010
+ ```
1011
+
1012
+ Font `src` values must start with `url(https://...)`. HTTP and data URIs are rejected.
1013
+
1014
+ ---
1015
+
1016
+ ## Billing details
1017
+
1018
+ ```ts
1019
+ interface BillingDetails {
1020
+ firstName: string; // 1–50 characters
1021
+ lastName: string; // 1–50 characters
1022
+ email?: string; // Valid email, max 50 characters
1023
+ phone?: string; // E.164 format, e.g. "+15551234567", max 50 characters
1024
+ address?: {
1025
+ line1: string; // 1–50 characters
1026
+ line2?: string; // Optional, omitted from cardSale if blank
1027
+ city: string; // 1–50 characters
1028
+ state: string; // For US/CA: normalized to 2-letter abbreviation
1029
+ zip: string; // 1–50 characters
1030
+ country: string; // ISO 3166-1 alpha-2, e.g. "US"
1031
+ };
1032
+ }
1033
+ ```
1034
+
1035
+ 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.
1036
+
1037
+ ---
1038
+
1039
+ ## Error handling
1040
+
1041
+ > πŸ“– [Error handling guide](https://docs.ozura.com/sdks/elements/error-handling) β€” `OzError` fields, every `errorCode` value, retry guidance, and session expiry behaviour.
1042
+
1043
+ All SDK errors are instances of `OzError`:
1044
+
1045
+ ```ts
1046
+ import { OzError } from '@ozura/elements';
1047
+
1048
+ try {
1049
+ const { token } = await vault.createToken({ billing });
1050
+ } catch (err) {
1051
+ if (err instanceof OzError) {
1052
+ switch (err.errorCode) {
1053
+ case 'network': // Connection failure β€” show retry UI
1054
+ case 'timeout': // 30s deadline exceeded β€” safe to retry
1055
+ case 'server': // 5xx from vault β€” transient, safe to retry
1056
+ if (err.retryable) showRetryPrompt();
1057
+ break;
1058
+
1059
+ case 'auth': // Bad pub key / API key β€” configuration issue
1060
+ case 'validation': // Bad card data β€” show field-level error
1061
+ case 'config': // frameBaseUrl not in permitted allowlist
1062
+ case 'unknown':
1063
+ showError(err.message);
1064
+ break;
1065
+ }
1066
+ }
1067
+ }
1068
+ ```
1069
+
1070
+ `OzError` fields:
1071
+
1072
+ | Field | Type | Description |
1073
+ |---|---|---|
1074
+ | `message` | `string` | Human-readable, consumer-facing error message. |
1075
+ | `errorCode` | `OzErrorCode` | `'network' \| 'timeout' \| 'auth' \| 'validation' \| 'server' \| 'config' \| 'unknown'` |
1076
+ | `raw` | `string` | Raw error string from the vault API, if available. |
1077
+ | `retryable` | `boolean` | `true` for `network`, `timeout`, `server`. `false` for `auth`, `validation`, `config`, `unknown`. |
1078
+
1079
+ > **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.
1080
+
1081
+ **Error normalisation helpers** (for displaying errors from `cardSale` to users):
1082
+
1083
+ ```ts
1084
+ import { normalizeVaultError, normalizeBankVaultError, normalizeCardSaleError } from '@ozura/elements';
1085
+
1086
+ // Maps vault tokenize error strings to user-facing copy
1087
+ const display = normalizeVaultError(err.raw); // card flows
1088
+ const display = normalizeBankVaultError(err.raw); // bank/ACH flows
1089
+ const display = normalizeCardSaleError(err.message); // cardSale API errors
1090
+ ```
1091
+
1092
+ ---
1093
+
1094
+ ## Debug mode
1095
+
1096
+ Pass `debug: true` in `VaultOptions` (or as a prop on `<OzElements>`) to activate structured console logging at every SDK lifecycle event.
1097
+
1098
+ ```ts
1099
+ const vault = await OzVault.create({
1100
+ pubKey: 'pk_live_...',
1101
+ sessionUrl: '/api/oz-session',
1102
+ debug: true, // enables [OzVault] console.log output
1103
+ });
1104
+ ```
1105
+
1106
+ ```tsx
1107
+ // React
1108
+ <OzElements pubKey="pk_live_..." sessionUrl="/api/oz-session" debug>
1109
+ ...
1110
+ </OzElements>
1111
+ ```
1112
+
1113
+ Each log entry is a `[OzVault] <message>` prefixed `console.log` call. Events logged include:
1114
+
1115
+ | Event | When it fires |
1116
+ |---|---|
1117
+ | `vault created` | Constructor completes |
1118
+ | `wax key received` | `fetchWaxKey` resolves |
1119
+ | `mounting tokenizer iframe` | Tokenizer iframe creation begins |
1120
+ | `tokenizer iframe ready` | Tokenizer iframe handshake complete |
1121
+ | `element iframe ready` | Each card/bank input iframe loads |
1122
+ | `field changed` | Per-field `change` event (empty/complete/valid/auto-advance state) |
1123
+ | `auto-advance` | Focus moves automatically between card fields |
1124
+ | `createToken() called` | Entry to `createToken()` |
1125
+ | `OZ_TOKENIZE sent` | Tokenize request dispatched to iframe |
1126
+ | `token received` | Token result returned (with elapsed ms) |
1127
+ | `token error` | Vault or network error during tokenize |
1128
+ | `proactive wax key refresh triggered` | Budget exhausted; refresh starting |
1129
+ | `wax key refresh started/succeeded/failed` | Refresh lifecycle |
1130
+ | `tab hidden` / `tab visible` | `visibilitychange` events |
1131
+ | `reset() called` | `vault.reset()` entry |
1132
+ | `destroy() called` | `vault.destroy()` entry |
1133
+
1134
+ **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.
1135
+
1136
+ ### vault.debugState()
1137
+
1138
+ `vault.debugState()` is always available β€” regardless of whether `debug: true` was set β€” and returns a one-time snapshot for attaching to bug reports:
1139
+
1140
+ ```ts
1141
+ console.log(vault.debugState());
1142
+ ```
1143
+
1144
+ Sample output:
1145
+
1146
+ ```json
1147
+ {
1148
+ "vaultId": "vault_abc12...",
1149
+ "isReady": true,
1150
+ "tokenizing": null,
1151
+ "destroyed": false,
1152
+ "waxKeyPresent": true,
1153
+ "tokenizeSuccessCount": 1,
1154
+ "maxTokenizeCalls": 3,
1155
+ "resetCount": 0,
1156
+ "elements": ["cardNumber", "expirationDate", "cvv"],
1157
+ "bankElements": [],
1158
+ "completionState": { "a1b2c3d4": true, "e5f6a7b8": true, "c9d0e1f2": false },
1159
+ "pendingTokenizations": 0,
1160
+ "pendingBankTokenizations": 0
1161
+ }
1162
+ ```
1163
+
1164
+ ---
1165
+
1166
+ ## Server utilities
1167
+
1168
+ > πŸ“– [Server SDK guide](https://docs.ozura.com/sdks/elements/server) β€” `Ozura` class methods, route handler factories, `getClientIp`, error types, and rate limits.
1169
+
1170
+ ### Ozura class
1171
+
1172
+ ```ts
1173
+ import { Ozura, OzuraError } from '@ozura/elements/server';
1174
+
1175
+ const ozura = new Ozura({
1176
+ merchantId: process.env.MERCHANT_ID!,
1177
+ apiKey: process.env.MERCHANT_API_KEY!,
1178
+ vaultKey: process.env.VAULT_API_KEY!,
1179
+ // apiUrl: 'https://api.ozura.com', // override Pay API URL
1180
+ // vaultUrl: 'https://vault.ozura.com', // override vault URL
1181
+ timeoutMs: 30000, // default
1182
+ retries: 2, // max retry attempts for 5xx/network errors (3 total attempts)
1183
+ });
1184
+ ```
1185
+
1186
+ > **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.
1187
+ >
1188
+ > ```ts
1189
+ > const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
1190
+ > ```
1191
+
1192
+ **Methods:**
1193
+
1194
+ ```ts
1195
+ // Charge a tokenized card
1196
+ const result = await ozura.cardSale({
1197
+ token: tokenResponse.token,
1198
+ cvcSession: tokenResponse.cvcSession,
1199
+ amount: '49.00',
1200
+ currency: 'USD', // default: 'USD'
1201
+ billing: tokenResponse.billing,
1202
+ clientIpAddress: '1.2.3.4', // fetch server-side, never from the browser
1203
+ // surchargePercent, tipAmount, salesTaxExempt, processor
1204
+ });
1205
+ // result.transactionId, result.amount, result.cardLastFour, result.cardBrand
1206
+ // result.surchargeAmount and result.tipAmount are optional β€” only present when non-zero
1207
+ const surcharge = result.surchargeAmount ?? '0.00';
1208
+ const tip = result.tipAmount ?? '0.00';
1209
+
1210
+ // Create a session key (for custom session endpoint implementations)
1211
+ const { sessionKey, expiresInSeconds } = await ozura.createSession({
1212
+ sessionId,
1213
+ sessionLimit: 3, // must match VaultOptions.sessionLimit on the client (default: 3)
1214
+ // pass null to remove the cap (vault default = unlimited)
1215
+ // Optional β€” stored in vault audit log for correlation with your own records:
1216
+ // orderId: order.id,
1217
+ // customerId: user.id,
1218
+ // cartId: cart.id,
1219
+ // metadata: { source: 'web' },
1220
+ // ttlSeconds: 600, // shorter TTL for quicker checkouts (default: 1800)
1221
+ });
1222
+
1223
+ // Revoke a session β€” call on all three session-end paths
1224
+ // Best-effort β€” never throws. Shortens the exposure window before the vault's ~30 min TTL.
1225
+ await ozura.revokeSession(sessionKey);
1226
+
1227
+ // Suggested pattern β€” wire all three exit paths:
1228
+ // 1. Payment success
1229
+ const result = await ozura.cardSale({ ... });
1230
+ await ozura.revokeSession(sessionKey); // session is spent; close the window immediately
1231
+
1232
+ // 2. User cancels checkout
1233
+ router.post('/api/cancel', async (req) => {
1234
+ const { sessionKey } = await db.session.get(req.sessionId);
1235
+ await ozura.revokeSession(sessionKey);
1236
+ return Response.json({ ok: true });
1237
+ });
1238
+
1239
+ // 3. Page/tab close (best-effort β€” browser may not deliver this)
1240
+ // Use sendBeacon so the request survives navigation / tab close.
1241
+ window.addEventListener('visibilitychange', () => {
1242
+ if (document.visibilityState === 'hidden') {
1243
+ navigator.sendBeacon('/api/cancel', JSON.stringify({ sessionId }));
1244
+ }
1245
+ });
1246
+
1247
+ // List transactions
1248
+ const { transactions, pagination } = await ozura.listTransactions({
1249
+ dateFrom: '2025-01-01',
1250
+ dateTo: '2025-12-31',
1251
+ transactionType: 'CreditCardSale',
1252
+ page: 1,
1253
+ limit: 50,
1254
+ });
1255
+ ```
1256
+
1257
+ **`OzuraError`** (thrown by all `Ozura` methods):
1258
+
1259
+ ```ts
1260
+ try {
1261
+ await ozura.cardSale(input);
1262
+ } catch (err) {
1263
+ if (err instanceof OzuraError) {
1264
+ err.statusCode; // HTTP status code
1265
+ err.message; // Normalized message
1266
+ err.raw; // Raw API response string
1267
+ err.retryAfter; // Seconds (only present on 429)
1268
+ }
1269
+ }
1270
+ ```
1271
+
1272
+ Rate limits: `cardSale` β€” 100 req/min per merchant. `listTransactions` β€” 200 req/min per merchant.
1273
+
1274
+ > **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.
1275
+
1276
+ ---
1277
+
1278
+ ### Route handler factories
1279
+
1280
+ The server package exports factory functions covering two runtimes Γ— two endpoints:
1281
+
1282
+ | Function | Runtime | Endpoint |
1283
+ |---|---|---|
1284
+ | `createSessionHandler` | Fetch API (Next.js App Router, Cloudflare, Vercel Edge) | `POST /api/oz-session` |
1285
+ | `createSessionMiddleware` | Express / Connect | `POST /api/oz-session` |
1286
+ | `createCardSaleHandler` | Fetch API | `POST /api/charge` |
1287
+ | `createCardSaleMiddleware` | Express / Connect | `POST /api/charge` |
1288
+
1289
+ `createCardSaleHandler` / `createCardSaleMiddleware` accept a `CardSaleHandlerOptions` object:
1290
+
1291
+ ```ts
1292
+ interface CardSaleHandlerOptions {
1293
+ /**
1294
+ * Required. Return the charge amount as a decimal string.
1295
+ * Never trust the amount from the request body β€” resolve it from your database.
1296
+ */
1297
+ getAmount: (body: Record<string, unknown>) => Promise<string>;
1298
+
1299
+ /**
1300
+ * Optional. Return the ISO 4217 currency code. Default: "USD".
1301
+ */
1302
+ getCurrency?: (body: Record<string, unknown>) => Promise<string>;
1303
+ }
1304
+ ```
1305
+
1306
+ 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.
1307
+
1308
+ ---
1309
+
1310
+ ## Local development
1311
+
1312
+ The repository includes a development server at `dev-server.mjs` that serves the built frame assets and proxies vault API requests:
1313
+
1314
+ ```bash
1315
+ npm run dev # build + start dev server on http://localhost:4242
1316
+ ```
1317
+
1318
+ Set `frameBaseUrl` to point your vault at the local server:
1319
+
1320
+ ```ts
1321
+ const vault = await OzVault.create({
1322
+ pubKey: 'pk_test_...',
1323
+ sessionUrl: '/api/oz-session',
1324
+ frameBaseUrl: 'http://localhost:4242', // local dev only
1325
+ });
1326
+ ```
1327
+
1328
+ Or in React:
1329
+
1330
+ ```tsx
1331
+ <OzElements
1332
+ pubKey="pk_test_..."
1333
+ sessionUrl="/api/oz-session"
1334
+ frameBaseUrl="http://localhost:4242"
1335
+ >
1336
+ ```
1337
+
1338
+ Configure environment variables for the dev server:
1339
+
1340
+ ```bash
1341
+ VAULT_URL=https://vault-staging.example.com
1342
+ VAULT_API_KEY=vk_test_...
1343
+ ```
1344
+
1345
+ ---
1346
+
1347
+ ## Content Security Policy
1348
+
1349
+ The SDK loads iframes from the Ozura frame origin. Add the following directives to your CSP:
1350
+
1351
+ ```
1352
+ frame-src https://elements.ozura.com;
1353
+ ```
1354
+
1355
+ If loading custom fonts via `fonts[].cssSrc`, also allow the font stylesheet origin:
1356
+
1357
+ ```
1358
+ style-src https://fonts.googleapis.com;
1359
+ font-src https://fonts.gstatic.com;
1360
+ ```
1361
+
1362
+ To verify your CSP after a build:
1363
+
1364
+ ```bash
1365
+ npm run check:csp
1366
+ ```
1367
+
1368
+ ---
1369
+
1370
+ ## TypeScript reference
1371
+
1372
+ > πŸ“– [API reference](https://docs.ozura.com/sdks/elements/api-reference) β€” every interface, union, and enum fully annotated with JSDoc.
1373
+
1374
+ All public types are exported from `@ozura/elements`:
1375
+
1376
+ ```ts
1377
+ import type {
1378
+ // Element types
1379
+ ElementType, // 'cardNumber' | 'cvv' | 'expirationDate'
1380
+ BankElementType, // 'accountNumber' | 'routingNumber'
1381
+ ElementOptions,
1382
+ ElementStyleConfig,
1383
+ ElementStyle,
1384
+ ElementChangeEvent,
1385
+
1386
+ // Vault config
1387
+ VaultOptions,
1388
+ FontSource,
1389
+ CssFontSource,
1390
+ CustomFontSource,
1391
+ Appearance,
1392
+ AppearanceVariables,
1393
+ OzTheme, // 'default' | 'night' | 'flat'
1394
+
1395
+ // Tokenization
1396
+ TokenizeOptions,
1397
+ BankTokenizeOptions,
1398
+ TokenResponse,
1399
+ BankTokenResponse,
1400
+ CardMetadata,
1401
+ BankAccountMetadata,
1402
+
1403
+ // Billing
1404
+ BillingDetails,
1405
+ BillingAddress,
1406
+
1407
+ // Card sale
1408
+ CardSaleRequest,
1409
+ CardSaleResponseData,
1410
+ CardSaleApiResponse,
1411
+
1412
+ // Transactions
1413
+ TransactionQueryParams,
1414
+ TransactionQueryPagination,
1415
+ TransactionQueryResponse,
1416
+ TransactionType,
1417
+ TransactionData,
1418
+ CardTransactionData,
1419
+ AchTransactionData,
1420
+ CryptoTransactionData,
1421
+
1422
+ // Errors
1423
+ OzErrorCode,
1424
+ } from '@ozura/elements';
1425
+ ```
1426
+
1427
+ Server-specific types are exported from `@ozura/elements/server`:
1428
+
1429
+ ```ts
1430
+ import type {
1431
+ OzuraConfig,
1432
+ CardSaleInput,
1433
+ CreateSessionOptions,
1434
+ CreateSessionResult,
1435
+ ListTransactionsInput,
1436
+ } from '@ozura/elements/server';
1437
+ ```
1438
+
1439
+ React-specific types are exported from `@ozura/elements/react`:
1440
+
1441
+ ```ts
1442
+ import type { OzFieldProps, OzCardProps, OzCardState, OzBankCardProps, OzBankCardState } from '@ozura/elements/react';
1443
+ ```
1444
+
1445
+ ---
1446
+
1447
+ ## Internal documentation
1448
+
1449
+ 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.
1450
+
1451
+ ---
1452
+
1453
+ ## Need help?
1454
+
1455
+ The full documentation β€” including interactive examples, a complete API reference, and integration walkthroughs β€” lives at:
1456
+
1457
+ **[docs.ozura.com/sdks/elements/overview](https://docs.ozura.com/sdks/elements/overview)**
1458
+
1459
+ | I want to… | Go to |
1460
+ |---|---|
1461
+ | Get started from scratch | [Installation](https://docs.ozura.com/sdks/elements/installation) |
1462
+ | Build a card payment form | [Card elements](https://docs.ozura.com/sdks/elements/card-elements) |
1463
+ | Build an ACH / bank form | [Bank elements](https://docs.ozura.com/sdks/elements/bank-elements) |
1464
+ | Use the React components | [React guide](https://docs.ozura.com/sdks/elements/react) |
1465
+ | Style the input fields | [Styling](https://docs.ozura.com/sdks/elements/styling) |
1466
+ | Handle errors correctly | [Error handling](https://docs.ozura.com/sdks/elements/error-handling) |
1467
+ | Set up the server side | [Server SDK](https://docs.ozura.com/sdks/elements/server) |
1468
+ | Look up a type or method | [API reference](https://docs.ozura.com/sdks/elements/api-reference) |