# API Reference Source: https://docs.ozura.com/api-reference/overview Complete API reference for Ozura's payment and vault services. Vault endpoint reference is available in the **Vault** section below. Checkout and OzuraPay API reference coming soon. ## Base URLs | Service | Sandbox | Production | | - | - | - | | OzuraPay API | `https://sandbox.payapi.v2.ozurapay.com` | `https://payapi.v2.ozurapay.com` | | Checkout | `https://sandbox-checkout.ozura.com` | `https://checkout.ozura.com` | | Vault | `https://api.ozuravault.com` (sandbox uses a test vault key) | `https://api.ozuravault.com` | Vault is a single environment — the same host serves test and production traffic, and the key (test vs. prod) determines which mode you're in. OzuraPay, by contrast, has fully separate **sandbox** and **production** environments at different hosts. See [Sandbox](/guides/sandbox) for the full setup. ## Authentication All API requests require authentication via API key or JWT token. See [Vault → Authentication](/api-reference/vault/authentication) for details. ## Vault API For Vault endpoint details (tokenize, proxy, verify API key, status codes, common errors), see **[Vault](/api-reference/vault/overview)** in the sidebar. # Application Management Source: https://docs.ozura.com/api-reference/vault/applications API reference for Vault applications — create, list, delete, API keys, permissions. # Applications Applications hold API keys used for tokenization and (optionally) provisioning. Management endpoints require a **JWT**; the verify-key endpoint uses the API key itself. ## Endpoints | Method | Path | Description | | - | - | - | | POST | `/api/applications` | Create application (returns API key once) | | GET | `/api/applications` | List applications | | GET | `/api/applications/{id}` | Get application details | | PUT | `/api/applications/{application_id}` | Update application (body: name, permissions) | | DELETE | `/api/applications/{id}` | Delete application | | GET | `/api/applications/key` | Verify API key (see [Verify API Key](/api-reference/vault/verify-api-key)) | ## POST /api/applications **Auth:** JWT with admin or owner role. **Request body:** | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Application name (3–50 characters) | | `permissions.tokenization.allowed` | boolean | Yes | Allow tokenize, detokenize, proxy | | `permissions.provisioning.allowed` | boolean | No | Allow creating sub-projects (default: false) | **Response (201):** `data` includes `id`, `name`, **`api_key`** (shown only once — store it securely), `permissions`, `created_at`. ## Permissions * **tokenization** — Create tokens, detokenize, use proxy, test tokens. * **provisioning** — Call `/internal/provision` to create sub-projects. ## Base URL Use the [Vault base URL](/api-reference/vault/overview) (sandbox or production). # Authentication Source: https://docs.ozura.com/api-reference/vault/authentication API reference for Vault authentication — API keys, JWT, and auth endpoints. Vault supports **API key** authentication (server-to-server) and **JWT** authentication (dashboard and management). **POST /tokenize** requires **X-Pub-Key** when using a production vault API key; test/sandbox API keys do not require it. Some requests support **hybrid** (API key + JWT for user attribution). ## API key **Header:** `X-API-Key: ` **Use for:** Tokenization, proxy, verify key. For **POST /tokenize** you must also send **X-Pub-Key** when using a production vault API key. Test/sandbox API keys do not require X-Pub-Key. **Endpoints that accept API key:** | Endpoint | Permission | | - | - | | POST /tokenize | tokenization (+ X-Pub-Key for production keys) | | POST /proxy/transaction | tokenization | | POST /test-tokens | tokenization (+ X-Pub-Key for production keys) | | GET /api/applications/key | any (verify key) | API key format may vary (e.g. provisioned keys); docs often show keys starting with `key_` as an example. Never expose API keys in client-side code. ## JWT **Header:** `Authorization: Bearer ` or cookie (after login). **Use for:** Dashboard and management — projects, applications, audit logs, user settings, MFA. JWT is required for management endpoints (projects, applications, audit logs, token management, user settings); [Applications](/api-reference/vault/applications) also documents the verify-key endpoint (API key). ### Auth endpoints (explicit paths) | Method | Path | Description | | - | - | - | | POST | /auth/login | Email/password login, returns JWT | | POST | /auth/logout | Log out | | GET | /auth/verify | Validate current JWT | | POST | /auth/change-password | Body: current\_password, new\_password (JWT) | | POST | /auth/forgot-password | Body: email | | GET | /auth/verify-reset-token | Query: token | | POST | /auth/reset-password | Body: token, new\_password | | POST | /auth/reset-expired-password | Body: token, current\_password, new\_password | | GET | /auth/password-expiry-info | JWT | | POST | /auth/send-verification-code | Email verification flow | | POST | /auth/verify-email-code | Verify email code | | POST | /auth/register | Registration (when email verification used) | | POST | /auth/mfa/verify | Body: code | | POST | /auth/mfa/verify-backup | Backup code | | POST | /auth/mfa/disable | Body: code | | GET | /auth/mfa/status | MFA status | | GET | /auth/mfa/verify-page | MFA verify page | | GET | /mfa-status | Root path for MFA status | **MFA setup** is via **POST /user/settings/mfa** (toggle/setup); response can include `totp_uri` and `backup_codes` when enabling. There is no separate /auth/mfa/setup path. ## Hybrid Send both `X-API-Key` and `Authorization: Bearer `. The API key authorizes the request; the JWT identifies the user for audit logging (e.g. on tokenize or proxy). ## Verify API key See [GET /api/applications/key](/api-reference/vault/verify-api-key) to validate a key and read its project and permissions. ## Base URL Use the [Vault base URL](/api-reference/vault/overview) (sandbox or production). # Common Errors Source: https://docs.ozura.com/api-reference/vault/common-errors Frequent Vault API errors and how to resolve them. Solutions for frequently encountered Vault API errors. ## Authentication | Message | Causes | Solutions | | - | - | - | | `Invalid API key` | Wrong key, deleted key, wrong environment | Verify key, check dashboard, use correct env | | `Token expired` | JWT expired | Re-authenticate via `/auth/login`, get new JWT | | `MFA verification required` | MFA not verified | Call `/auth/mfa/verify` or use backup code | ## Tokens and CVC | Message | Causes | Solutions | | - | - | - | | `Token not found` | Invalid token, wrong project, deleted | Check token ID, project, and that token exists | | `CVC session expired or not found` | >90 sec, already used, invalid ID | Re-tokenize with CVV for new session | | `Invalid card number` | Luhn fail, wrong length, non-numeric | Card must be 13–19 digits; validate Luhn, strip spaces/dashes | | `Card has expired` | Past expiration | Use current or future expiry | ## Permissions | Message | Causes | Solutions | | - | - | - | | `This API key does not have permission for this operation` | Key lacks permission | Check app permissions in dashboard | | `Only admins and owners can...` | Insufficient role | Use admin/owner user or request access | ## Rate limits | Message | Solution | | - | - | | `Rate limit exceeded` | Wait `retry_after_seconds`; use exponential backoff | | `Too many failed attempts` | Wait for lockout; verify credentials before retrying | ## Proxy | Message | Causes | Solutions | | - | - | - | | `Failed to proxy request` | Bad URL, PSP down, network | Check `proxy_url`, PSP status, retry | | `Invalid proxy URL` | Malformed or non-HTTPS | Use full `https://` URL | ## Validation | Message | Solutions | | - | - | | `Missing required field: X` | Include all required fields; check JSON shape | | `Invalid value for X` | Check type (string vs number) and format per [API Reference](/api-reference/vault/overview) | ## Getting help If the error persists: check [Guides](/guides/vault/overview), review [audit logs](/guides/vault/audit-logs/overview), and contact support with error message, redacted request, timestamp, and project ID. # Errors Overview Source: https://docs.ozura.com/api-reference/vault/errors-overview Vault API error format and where to find status codes and common errors. Vault returns errors as JSON with `success: false` and a `message` field. Use the HTTP status code and message to decide whether to retry, fix the request, or prompt the user. ## Response format ```json theme={null} { "success": false, "message": "Human-readable error message", "data": null } ``` ## Reference | Page | Content | | - | - | | [HTTP status codes](/api-reference/vault/status-codes) | All status codes and typical causes | | [Common errors](/api-reference/vault/common-errors) | Frequent messages and how to fix them | ## Quick reference | Code | Meaning | | - | - | | 400 | Bad Request — fix input | | 401 | Unauthorized — check API key or JWT | | 403 | Forbidden — permissions or MFA | | 404 | Not Found — resource doesn't exist | | 409 | Conflict — duplicate or state conflict | | 429 | Too Many Requests — back off; use `retry_after_seconds` if present | | 5xx | Server/upstream error — retry with backoff | # Vault API Overview Source: https://docs.ozura.com/api-reference/vault/overview Base URL and authentication for Ozura Vault API. # Vault API Reference for Ozura Vault endpoints. For integration steps and concepts, see the [Vault Guides](/guides/vault/overview). Deployed backends may use different hostnames (e.g. Azure); the path segments below are correct regardless. There is no version prefix (e.g. no `/v1`). ## Authentication All requests require one of: * **API Key** — `X-API-Key` header (server-to-server). Get an API key by creating an account at [Ozura Vault](https://ozuravault.com). **POST /tokenize** and **POST /test-tokens** also require **X-Pub-Key** when using a production vault API key. If your API key is a test/sandbox key, `X-Pub-Key` is not required. For a production pub key, contact **[ammar@ozura.com](mailto:ammar@ozura.com)**. See [Verify API Key](/api-reference/vault/verify-api-key). * **JWT** — `Authorization: Bearer ` or cookie (user context). Details: [Authentication](/api-reference/vault/authentication). ## Endpoints | Method | Path | Description | | - | - | - | | POST | [/tokenize](/api-reference/vault/tokenize) | Create a token from card or bank data | | POST | [/proxy/transaction](/api-reference/vault/proxy-transaction) | Forward request to a PSP with card data injected | | GET | [/api/applications/key](/api-reference/vault/verify-api-key) | Verify API key and read permissions | | — | [Authentication](/api-reference/vault/authentication) | API key vs JWT, which endpoints use which | \| POST | [Test tokens](/api-reference/vault/test-tokens) | Create test token with long-lived CVC (X-Pub-Key + API key/JWT) | \| GET/POST/PUT/DELETE | [Applications](/api-reference/vault/applications) | Create, list, update, delete apps; verify key (API key) | ## Errors * [Errors overview](/api-reference/vault/errors-overview) * [HTTP status codes](/api-reference/vault/status-codes) * [Common errors and solutions](/api-reference/vault/common-errors) # Proxy Source: https://docs.ozura.com/api-reference/vault/proxy-transaction Forward a request to a PSP with card data injected from a token. # POST /proxy/transaction Send a request to your payment processor (PSP) with card data injected from a token. Your server never sees raw card data. Supports JSON, form-encoded, and XML bodies. For JSON, send `token`, `cvc_session_id`, and `proxy_url` in the body; for form/XML you may also send them via headers **X-Token**, **X-Proxy-URL**, and **X-CVC-Session-ID** (or in query/form). ## Request ```json theme={null} { "token": "tok_a1b2c3d4e5f6", "cvc_session_id": "550e8400-e29b-41d4-a716-446655440000", "proxy_url": "https://api.stripe.com/v1/charges", "request_data": { "amount": 2000, "currency": "usd", "source": { "number": "${cardNumber}", "exp_month": "${expirationMonth}", "exp_year": "${expirationYear}", "cvc": "${cvv}" } }, "http_headers": { "Authorization": "Bearer sk_test_xxx" } } ``` ### Request body | Field | Type | Required | Description | | - | - | - | - | | `token` | string | Yes | Token to use | | `cvc_session_id` | string | No | CVC session for CVV (if tokenized with CVV) | | `proxy_url` | string | Yes | Full PSP endpoint URL (HTTPS) | | `request_data` | object | Yes | Request body with placeholders (see below) | | `http_headers` | object | No | Headers to send (e.g. PSP API key) | | `http_method` | string | No | HTTP method (default: POST) | ### Placeholders in `request_data` Replaced with real card data before the request is sent: | Placeholder | Value | | - | - | | `${cardNumber}` | Full card number | | `${expirationMonth}` | 2-digit month (01–12) | | `${expirationYear}` | 2- or 4-digit year | | `${cvv}` or `${cvc}` | Security code (if `cvc_session_id` provided) | Bank: `${accountNumber}`, `${routingNumber}`. ## Response ### 200 OK Proxy returns the PSP response wrapped: ```json theme={null} { "success": true, "proxy_response": { "status_code": 200, "headers": { "content-type": "application/json" }, "body": { "id": "ch_1234567890", "object": "charge", "amount": 2000, "status": "succeeded" } } } ``` ## Error responses | Status | Example message | | - | - | | 400 | `Invalid proxy URL` | | 404 | `Token not found`, `CVC session expired or not found` | | 502 | `Failed to proxy request` (upstream error) | | 504 | `Request timeout` | ## Delete CVC session **DELETE /proxy/delete-cvc-session/** — Delete a CVC session by UUID (e.g. after a successful transaction). **Auth:** API key. Response: `{ "success": true, "message": "CVC session deleted!" }`. Production CVC sessions have a **90-second TTL** and are deleted after use in proxy; use this endpoint to delete explicitly when needed (e.g. test sessions). See [Status codes](/api-reference/vault/status-codes) and [Common errors](/api-reference/vault/common-errors). # HTTP Status Codes Source: https://docs.ozura.com/api-reference/vault/status-codes Status codes returned by Ozura Vault API. Reference for HTTP status codes returned by Vault endpoints. ## Success | Code | Status | Description | | - | - | - | | 200 | OK | Request succeeded | | 201 | Created | Resource created (e.g. token) | ## Client errors | Code | Status | Common causes | | - | - | - | | 400 | Bad Request | Invalid JSON, missing/invalid fields, invalid card number | | 401 | Unauthorized | Missing or invalid API key, expired JWT | | 403 | Forbidden | MFA required, insufficient permissions, account locked | | 404 | Not Found | Token or resource doesn't exist or was deleted | | 409 | Conflict | Duplicate resource (e.g. application name) | | 429 | Too Many Requests | Rate limit exceeded; use `retry_after_seconds` if present | ## Server errors | Code | Status | Action | | - | - | - | | 500 | Internal Server Error | Retry with backoff | | 502 | Bad Gateway | Proxy/upstream error; retry | | 503 | Service Unavailable | Retry after delay | | 504 | Gateway Timeout | Upstream timeout; retry | ## Example error body ```json theme={null} { "success": false, "message": "Human-readable error message", "data": null } ``` For specific messages and troubleshooting, see [Common errors](/api-reference/vault/common-errors). For integration patterns, see [Error handling](/guides/vault/errors/overview). # Test Tokens Source: https://docs.ozura.com/api-reference/vault/test-tokens Create a test card token with a long-lived CVC session (1-year TTL). # POST /test-tokens Create a test card token with a **long-lived CVC session** (1-year TTL). Test CVC sessions are not deleted after use in proxy (unlike production CVC sessions). Use for development and testing. **Auth:** **X-API-Key** (or JWT) is always required. **X-Pub-Key** is required when using a production vault API key; test/sandbox API keys do not require it. For a production pub key, contact **[ammar@ozura.com](mailto:ammar@ozura.com)**. ## Request ```json theme={null} { "cardNumber": "4111111111111111", "expirationMonth": "12", "expirationYear": "2025", "cvc": "123", "firstName": "Test", "lastName": "User" } ``` | Field | Type | Required | Description | | - | - | - | - | | `cardNumber` | string | Yes | Test card number | | `expirationMonth` | string | Yes | 2-digit month (01–12) | | `expirationYear` | string | Yes | 2- or 4-digit year | | `cvc` | string | Yes | Security code | | `firstName` | string | No | Cardholder first name | | `lastName` | string | No | Cardholder last name | ## Response **201 Created.** Includes `token`, `cvc_session_id`, `mask`, `bin`, `test_mode: true`, and `expires_in` (e.g. `"1 year (test session)"`). The CVC session is long-lived and is **not** deleted after use in proxy. See [POST /tokenize](/api-reference/vault/tokenize) for production tokenization. CVC sessions from `/tokenize` have a **90-second TTL** and are deleted after use. # Tokenize Source: https://docs.ozura.com/api-reference/vault/tokenize Create a token from card or bank account data. # POST /tokenize Create a token from sensitive card or bank account data. Card data can include an optional CVV, which is stored in a separate **CVC session** (90-second TTL; deleted after use in proxy). **Auth:** **X-API-Key** (or JWT) is always required. **X-Pub-Key** is required when using a production vault API key; test/sandbox API keys do not require it. Get an API key by creating an account at [Ozura Vault](https://ozuravault.com). For a production pub key, contact **[ammar@ozura.com](mailto:ammar@ozura.com)**. ## Request ```json theme={null} { "type": "card", "data": { "cardNumber": "4111111111111111", "expirationMonth": "12", "expirationYear": "2029", "cvv": "123" } } ``` ### Request body | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | `"card"` or `"bank"` | | `data.cardNumber` | string | Yes (card) | 13–19 digit card number | | `data.expirationMonth` | string | Yes (card) | 2-digit month (01–12) | | `data.expirationYear` | string | Yes (card) | 2- or 4-digit year | | `data.cvv` | string | No | 3–4 digit security code | | `data.cvc` | string | No | Alias for `cvv` | ### CVV/CVC handling * If `cvv` or `cvc` is provided: stored in a separate CVC session, **90-second TTL**, deleted after use in proxy; response includes `cvc_session_id`. * If omitted: no CVC session; no `cvc_session_id` in response; CVC cannot be used in proxy. For long-lived test CVC sessions (e.g. 1-year), use [POST /test-tokens](/api-reference/vault/test-tokens). ## Response ### 201 Created ```json theme={null} { "success": true, "token": "tok_a1b2c3d4e5f6g7h8i9j0", "mask": { "card_number": "411111******1111" }, "cvc_session_id": "550e8400-e29b-41d4-a716-446655440000", "bin": "411111" } ``` | Field | Type | Description | | - | - | - | | `success` | boolean | Request success | | `token` | string | Token identifier | | `data` | object | Masked payload (e.g. `number`, `expiration_month`, `expiration_year`) for display | | `mask.card_number` | string | Masked card for display | | `cvc_session_id` | string | Present if CVV was sent | | `bin` | string | First 6 digits (BIN) | | `fingerprint` | string | If deduplication enabled | | `deduplicated` | boolean | If existing token was returned | Request may include optional `maskingConfig` for custom masking behavior. ## Validation * **Card number:** 13–19 digits, Luhn valid; spaces/dashes stripped. * **Expiration:** month 01–12; year current or future. * **CVV:** 3 digits (Visa, MC, Discover), 4 (Amex). ## Error responses | Status | Example message | | - | - | | 400 | `Invalid card number`, `Card has expired`, `Missing required field: cardNumber` | | 401 | `Invalid API key` | | 403 | Permission denied | See [Status codes](/api-reference/vault/status-codes) and [Common errors](/api-reference/vault/common-errors). # Verify Key Source: https://docs.ozura.com/api-reference/vault/verify-api-key Verify an API key and read its project and permissions. # GET /api/applications/key Validate an API key and return associated project and application info and permissions. ## Request **Headers** | Header | Required | Description | | - | - | - | | `X-API-Key` | Yes | The API key to verify | No request body. ## Response ### 200 OK ```json theme={null} { "success": true, "message": "API key is valid", "data": { "project_id": "proj_xyz789", "application_id": "app_abc123", "name": "My Application", "permissions": { "tokenization": { "allowed": true }, "provisioning": { "allowed": false } } } } ``` ## Error responses | Status | Example message | | - | - | | 401 | `Invalid API key` | See [Status codes](/api-reference/vault/status-codes). # API Reference Source: https://docs.ozura.com/guides/payments/checkout/api-reference Complete reference for all Ozura Checkout API endpoints Complete reference for all Ozura Checkout API endpoints. ## Base URL ``` https://checkout.ozura.com ``` **Environments:** Use `https://checkout.ozura.com` for production. For staging, use the URL provided by your Ozura representative. ## Authentication All requests require these headers: | Header | Description | | :- | :- | | `X-API-KEY` | Your Vault API Key | | `X-OZURA-API-KEY` | Your Merchant API Key | | `Content-Type` | `application/json` | ## Rate Limits Rate limits are applied per API key to ensure fair usage. If you exceed rate limits, you'll receive a `429 Too Many Requests` response. ## Create Session Creates a checkout session and returns a checkout URL. ``` POST /api/sessions/create ``` ### Request Body | Field | Type | Required | Default | Description | | :- | :- | :-: | :- | :- | | `merchantId` | string | **Yes** | — | Your Merchant ID | | `merchantName` | string | **Yes** | — | Business name shown to customer | | `amount` | string/number | **Yes**\* | — | Payment amount in major currency units (dollars for USD, not cents): `"25.00"` charges \$25.00 | | `currency` | string | No | `"USD"` | Currency code | | `successUrl` | string | **Yes** | — | Redirect after successful payment | | `cancelUrl` | string | **Yes** | — | "Go back" destination | | `errorUrl` | string | **Yes** | — | Redirect if payment fails | | `embedMode` | string | No | `"redirect"` | `"redirect"`, `"popup"`, or `"new_tab"`. `"iframe"` is coming soon; for now it is accepted but runs as `"redirect"` with an `EMBED DOWNGRADE` warning. See [Integration Modes](/guides/payments/checkout/integration-modes#iframe-mode-coming-soon) | | `parentOrigin` | string | Cond. | — | Origin of the page that opens checkout (e.g. `"https://yoursite.com"`). Required for `embedMode: "popup"`; needed for `"new_tab"` to receive events. Result events are sent only to this origin | | `checkoutMode` | string | No | `"payment"` | `"payment"`, `"donation"`, or `"recurring"` | | `recurringConfig` | object | Cond. | — | Required when `checkoutMode: "recurring"`. See [Recurring Mode](/guides/payments/checkout/recurring-mode) | | `donationConfig` | object | No | — | Donation settings. See [Donation Mode](/guides/payments/checkout/donation-mode) | | `surchargePercent` | string/number | No | No surcharge (treated as `0%` at charge time) | Credit card surcharge, max `"3.00"` (3%). Shown as a line item when set | | `transactionChannel` | string | No | `"ecommerce"` | `"ecommerce"` or `"moto"`. Forwarded to the payment processor | | `taxExempt` | boolean | No | `false` | Skip sales tax calculation for this session | | `items` | array | No | — | Cart items (alternative to `amount`). Not allowed with `recurring` mode | | `metadata` | object | No | `{}` | Custom data (max 10KB), returned in the success URL | | `appearance` | object | No | `{}` | Styling options — see [Appearance Reference](/guides/payments/checkout/appearance-reference) | | `idempotencyKey` | string | No | — | Unique key for network retry protection | | `allowPopup` | boolean | No | `true` | Accepted for compatibility; currently has no effect | | `hideHeader` | boolean | No | `false` | Hide the merchant name / logo header | | `donationConfig` | object | No | — | Donation preset amounts and limits. See [Donation Mode](/guides/payments/checkout/donation-mode) | \*`amount` is required when `checkoutMode` is `"payment"` or `"recurring"` and `items` is not provided. Not required for `"donation"` mode. **Deprecated fields:** `taxRate` and `shippingCost` are accepted and validated but **ignored** — they have no effect on pricing. Do not send these fields in new integrations. ### Response ```json theme={null} { "success": true, "data": { "sessionId": "session_xxxxxxxxxxxxxx", "checkoutUrl": "https://checkout.ozura.com/checkout/session_xxxxxxxxxxxxxx", "expiresAt": "2025-01-07T12:15:00.000Z", "embedCode": "", "pricing": { "subtotal": 55.00, "tax": 0, "shipping": 0, "total": 55.00, "currency": "USD" } } } ``` The response may also include a top-level `"warnings"` array with advisory messages, for example an `EMBED DOWNGRADE` message when you request `embedMode: "iframe"` (the session then runs as `"redirect"`). On an idempotent replay (same `idempotencyKey`), `"cached": true` is added at the top level and a new session is not created. ## Get Session Retrieve session details. The session ID acts as the access token — no additional authentication headers are required. ``` GET /api/sessions/{sessionId} ``` ### Response The full session object is returned. Key fields include: ```json theme={null} { "success": true, "data": { "session": { "id": "session_xxxxxxxxxxxxxx", "status": "pending", "amount": "25.00", "currency": "USD", "merchantId": "your_merchant_id", "checkoutMode": "payment", "embedMode": "redirect", "taxExempt": false, "appearance": {}, "metadata": { "merchantName": "My Store", "orderId": "order_123" }, "recurringConfig": null, "surchargePercent": null, "transactionChannel": "ecommerce", "expiresAt": "2025-01-07T12:15:00.000Z", "createdAt": "2025-01-07T11:45:00.000Z" }, "pricing": { "subtotal": 25.00, "tax": 0, "shipping": 0, "total": 25.00, "currency": "USD" }, "items": [] } } ``` `merchantName` is stored inside `metadata`, not as a top-level session field. For recurring sessions, `recurringConfig` contains the full plan config object. The `metadata` object may contain internal fields used by the checkout page — do not expose, log, or forward the full `metadata` object to clients. ### Session Statuses | Status | Description | | :- | :- | | `pending` | Awaiting payment | | `completed` | Payment successful | | `cancelled` | Customer cancelled | | `expired` | Session timed out | | `failed` | Payment failed | ## Cancel Session Cancels a pending session. This endpoint is called **automatically** by the Ozura checkout page when the customer clicks "Cancel" — you do not need to call it from your server. ``` POST /api/sessions/{sessionId}/cancel ``` This endpoint is browser-facing. It is called by the checkout page using a short-lived internal session credential. It cannot be called from your server using your Vault API Key — requests with `X-API-KEY` are rejected. If a session is not cancelled explicitly, it expires automatically after 30 minutes. ### Response ```json theme={null} { "success": true } ``` ## Create Payment Link Creates a reusable payment URL. ``` POST /api/payment-links/create ``` ### Request Body | Field | Type | Required | Default | Description | | :- | :- | :-: | :- | :- | | `merchantId` | string | **Yes** | — | Your Merchant ID | | `merchantName` | string | **Yes** | — | Business name shown to customer | | `amount` | string | **Yes**\* | — | Payment amount | | `currency` | string | No | `"USD"` | Currency code | | `successUrl` | string | **Yes** | — | Redirect after payment | | `cancelUrl` | string | **Yes** | — | Redirect if cancelled | | `errorUrl` | string | **Yes** | — | Redirect if failed | | `checkoutMode` | string | No | `"payment"` | `"payment"`, `"donation"`, or `"recurring"` | | `recurringConfig` | object | Cond. | — | Required when `checkoutMode: "recurring"`. See [Recurring Mode](/guides/payments/checkout/recurring-mode) | | `donationConfig` | object | No | — | Donation settings. See [Donation Mode](/guides/payments/checkout/donation-mode) | | `surchargePercent` | string/number | No | No surcharge | Credit card surcharge, max `"3.00"` (3%) | | `transactionChannel` | string | No | `"ecommerce"` | `"ecommerce"` or `"moto"` | | `taxExempt` | boolean | No | `false` | Skip sales tax calculation for this link's sessions | | `embedMode` | string | No | `"redirect"` | `"redirect"`, `"popup"`, `"iframe"`, `"new_tab"`. Stored on the link but **payment links always open checkout as redirect** — this field is persisted for informational purposes only | | `parentOrigin` | string | Cond. | — | Required if `embedMode` is `"popup"` or `"iframe"` (validation only; payment links always open as redirect) | | `allowPopup` | boolean | No | `true` | Accepted for compatibility; currently has no effect | | `hideHeader` | boolean | No | `false` | Hide the merchant name / logo header | | `items` | array | No | — | Cart items (alternative to `amount`) | | `metadata` | object | No | `{}` | Custom data (max 10KB) | | `merchantPayLinkReference` | string | No | — | Link-scoped correlation ID returned on **every** transaction from this link, inside the success `metadata`. Max 128 chars: letters, numbers, `. _ : -`. | | `appearance` | object | No | `{}` | Styling options — see [Appearance Reference](/guides/payments/checkout/appearance-reference) | | `idempotencyKey` | string | No | — | Unique key for network retry protection | | `expiresInDays` | number | No | `7` | Days until link expires | | `usageLimit` | number | No | `null` (unlimited) | Max successful payments before the link is deactivated | \*Required when `checkoutMode` is `"payment"` or `"recurring"` and `items` is not provided. Not required for donation mode. **Correlating payments to your orders.** Payment links support three correlation references, all returned in the success-redirect `metadata` (see [Correlate Payments to Your Orders](/guides/payments/checkout/payment-links#correlate-payments-to-your-orders) for the full guide): * `merchantPayLinkReference` — set here at link creation; static, returned on **every** transaction from the link (attribute a link/campaign). * `merchantReference` — set **per checkout** via a `?ref=` query parameter appended to the link URL (not a request-body field); unique per payer. * `merchantRecurringReference` — for recurring links, set inside [`recurringConfig`](/guides/payments/checkout/recurring-mode); also persisted on the plan so you can look the plan up by it. ### Response ```json theme={null} { "success": true, "data": { "paymentLinkId": "pl_xxxxxxxxxxxxxx", "url": "https://checkout.ozura.com/pay/pl_xxxxxxxxxxxxxx", "expiresAt": "2025-01-14T00:00:00.000Z" } } ``` On an idempotent replay (same `idempotencyKey`), the response also includes `"cached": true`. Either response may include a top-level `"warnings"` array with merchant-facing advisory messages. ## Check Payment Link Availability Check if a payment link is still valid. No authentication required. ``` GET /api/payment-links/{linkId}/check-availability ``` ### Response (Available) ```json theme={null} { "success": true, "available": true, "message": "Payment link is available" } ``` ### Response (Unavailable) ```json theme={null} { "success": false, "available": false, "error": "Payment link usage limit reached" } ``` ## Error Responses All errors follow this format: ```json theme={null} { "success": false, "error": "Human-readable error message", "details": ["Field-level detail — only present for validation errors"] } ``` `details` is only included when there are multiple specific validation failures (e.g. several invalid fields at once). Single-field and non-validation errors return `error` only. ### Common Error Codes | Code | Meaning | | :- | :- | | `400` | Bad request (validation failed) | | `401` | Invalid or missing API credentials | | `404` | Session not found | | `409` | Conflict — session already completed, or `idempotencyKey` reuse rejected (see below) | | `410` | Session expired | | `429` | Too many requests (rate limit exceeded) | | `500` | Server error (retry recommended) | ### Idempotency Conflict Codes When you replay a request with an `idempotencyKey` that does not match the original active session, the API returns `409` with one of these machine-readable `errorCode` values: | `errorCode` | Meaning | | :- | :- | | `IDEMPOTENCY_KEY_EXPIRED_SESSION` | The session previously created with this `idempotencyKey` has expired. Use a new `idempotencyKey` to create a fresh session | | `IDEMPOTENCY_KEY_TERMINAL_SESSION` | The session for this `idempotencyKey` is in a terminal state (`cancelled` or `failed`). Use a new `idempotencyKey` | # Customize Appearance Source: https://docs.ozura.com/guides/payments/checkout/appearance Brand the checkout page with your colors and logo Brand the hosted checkout page with your colors, logo, and typography. Pass an `appearance` object when you [create a session](/guides/payments/checkout/create-session) or [payment link](/guides/payments/checkout/payment-links). **Current checkout layout:** On desktop, the customer sees a **soft outer page background** (default lavender) with a **single centered card** (\~960px): **payment form on the left** (\~56%) and **order summary on the right** (\~44%), plus a **brand bar** above the card. On mobile, branding and summary stack above the form. There is no API flag to switch to a different layout. The full field list and edge cases (including fields accepted by the API but not yet used in the UI) are in the [Appearance Reference](/guides/payments/checkout/appearance-reference). ## Basic Customization ```json theme={null} { "merchantId": "your_merchant_id", "merchantName": "My Store", "amount": "25.00", "successUrl": "https://mystore.com/success", "cancelUrl": "https://mystore.com/cancel", "errorUrl": "https://mystore.com/error", "appearance": { "primaryColor": "#4F46E5", "buttonBackgroundColor": "#4F46E5", "textColor": "#111827" } } ``` `merchantName` is **required** when creating a session; it is stored on the session and drives the **store title** next to your logo (it is not set through `appearance` alone). ## Appearance Options (common) ### Colors | Option | Description | Example | | :- | :- | :- | | `primaryColor` | Main brand color (accents, selected payment method border, default pay button chain) | `"#4F46E5"` | | `primaryHoverColor` | Primary hover | `"#4338CA"` | | `secondaryColor` | Secondary accents where used | `"#64748B"` | | `backgroundColor` | Sets global/custom fallbacks (inputs, many panels). **Not** the outer lavender canvas behind the desktop card—use `cssVariables` with key `page-background` for that (see reference). | `"#FFFFFF"` | | `textColor` | Default text | `"#111827"` | | `borderColor` | Borders and some dividers | `"#D1D5DB"` | | `buttonBackgroundColor` | Pay button background (overrides `primaryColor` for the button) | `"#4F46E5"` | | `buttonHoverColor` | Pay button hover | `"#4338CA"` | ### Logo | Option | Description | Example | | :- | :- | :- | | `logoUrl` | HTTPS URL to your logo | `"https://mystore.com/logo.png"` | | `logoPosition` | `inline` (default) or `top` | `"inline"` | | `logoMaxHeight` | Present in the API model; **not applied** by the live page today (logo size follows `logoPosition` and viewport). | `"50px"` | | `showMerchantName` | If `true`, show name with logo. If omitted/false with a logo, **logo only**. Without `logoUrl`, the **name always shows**. | `true` | ### Layout (inside the desktop card) | Option | Description | Example | | :- | :- | :- | | `leftSideBackground` | **Order summary** column (visually the **right** side of the card) | `"#F9FAFB"` | | `rightSideBackground` | **Payment form** column (visually the **left** side of the card) | `"#FFFFFF"` | | `formBackground` | Form container / panel fill | `"#FFFFFF"` | | `mobileBackground` | Mobile sections (summary, donation block, etc.) | `"#FFFFFF"` | | `borderRadius` | Corner scale for themed controls | `"lg"` | Valid `borderRadius` values: `none`, `sm`, `base`, `lg`, `xl`, `full`. ## Example: Dark panels ```json theme={null} { "appearance": { "primaryColor": "#8B5CF6", "buttonBackgroundColor": "#8B5CF6", "buttonHoverColor": "#7C3AED", "backgroundColor": "#1F2937", "textColor": "#F9FAFB", "borderColor": "#374151", "leftSideBackground": "#111827", "rightSideBackground": "#1F2937", "formBackground": "#374151" } } ``` ## Example: Brand colors + logo ```json theme={null} { "appearance": { "primaryColor": "#FF6B00", "primaryHoverColor": "#E55D00", "logoUrl": "https://mystore.com/logo.png", "showMerchantName": true } } ``` ## Outer page background (desktop) The area **behind** the centered card uses CSS variable `--custom-page-background` (default lavender). Set it via `appearance.cssVariables`: ```json theme={null} { "appearance": { "primaryColor": "#4F46E5", "cssVariables": { "page-background": "#EEF2FF" } } } ``` Values in `cssVariables` are sanitized as **colors or size/dimension values** (e.g. `px`, `em`, `%`). Values that don't parse as either are silently dropped. Use `customCss` for arbitrary styling (within [security limits](/guides/payments/checkout/appearance-reference#custom-css)). As a shorthand, you can also set `pageBackground` directly in the `appearance` object instead of via `cssVariables`: ```json theme={null} { "appearance": { "primaryColor": "#4F46E5", "pageBackground": "#EEF2FF" } } ``` Both forms are equivalent. `cssVariables.page-background` takes precedence if both are set. ## Full options See the [Appearance Reference](/guides/payments/checkout/appearance-reference) for every key, mobile overrides, cart colors, `customCss`, accessibility warnings, and limitations (for example **pay button label** is derived from tax context—not a single `appearance` string). # Appearance Reference Source: https://docs.ozura.com/guides/payments/checkout/appearance-reference Complete list of all checkout appearance customization options Complete list of `appearance` fields for Ozura Checkout. The hosted page uses a **centered card** layout on desktop (form left, summary right inside the card; brand bar above). Field names match the Checkout API. **Naming note:** `leftSideBackground` styles the **order summary** panel (the visually **right** column inside the card). `rightSideBackground` styles the **payment form** panel (the visually **left** column). The names are historical; use the descriptions below, not the word “left/right” alone. ## Global Colors | Key | Type | Description | | :- | :- | :- | | `primaryColor` | Hex | Main brand color (accents, active payment-method border, default pay button chain) | | `primaryHoverColor` | Hex | Hover for primary-colored controls | | `secondaryColor` | Hex | Secondary accent where used | | `backgroundColor` | Hex | Sets global/custom fallbacks (inputs, many panels). **Not** the outer lavender canvas—use `cssVariables.page-background` (see [Customize Appearance](/guides/payments/checkout/appearance)). | | `textColor` | Hex | Default text | | `borderColor` | Hex | Borders and some dividers | ## Layout (Desktop — inside the card) | Key | Type | Description | | :- | :- | :- | | `leftSideBackground` | Hex | **Order summary** panel background (right portion of the card) | | `rightSideBackground` | Hex | **Payment form** panel background (left portion of the card) | | `formBackground` | Hex | Form container background | | `dividerColor` | Hex | Divider color; on mobile, used with `showTopDivider` for a line under the top section | | `showTopDivider` | Boolean | When `true` and `dividerColor` is set, draws a divider under the mobile header area | ## Layout (Mobile) | Key | Type | Description | | :- | :- | :- | | `mobileBackground` | Hex | Mobile sections (summary, donation block, etc.) | | `mobileTopBackground` | Hex | Top branding / header band | | `mobileBottomBackground` | Hex | Lower sections where applied | | `mobileFormBackgroundColor` | Hex | Form area background | | `mobileStickyTop` | Boolean | If `true`, pins the mobile branding header and order summary to the top of the viewport as the customer scrolls down to fill in the form | ## Typography | Key | Type | Description | | :- | :- | :- | | `fontFamily` | String | Font stack (sanitized); does **not** style the fixed “Powered by OzuraPay” footer | | `fontSize` | String | `sm`, `base`, `lg`, `xl` (drives scaled sizing on the page) | | `fontWeight` | String | In API/types; **not applied** by checkout today | ## Spacing & Borders | Key | Type | Description | | :- | :- | :- | | `spacing` | String | `compact`, `normal`, `spacious` | | `borderRadius` | String | `none`, `sm`, `base`, `lg`, `xl`, `full` | ## Text Colors | Key | Type | Description | | :- | :- | :- | | `headingTextColor` | Hex | Titles and headings (including merchant name styling chain) | | `bodyTextColor` | Hex | Body text where wired | | `labelTextColor` | Hex | Form labels | | `formLabelTextColor` | Hex | Labels inside card/ACH sections | | `secondaryTextColor` | Hex | Secondary/helper text | | `amountTextColor` | Hex | Amount displays | | `emailTextColor` | Hex | Email field | | `cardTextColor` | Hex | Card payment fields | | `achTextColor` | Hex | ACH / bank fields | ## Input Fields | Key | Type | Description | | :- | :- | :- | | `inputBackgroundColor` | Hex | Input background | | `inputBorderColor` | Hex | Default border (the new layout often uses borderless inputs with a focus ring; variable still applies where used) | | `inputFocusBorderColor` | Hex | Focus border where used | | `inputErrorBorderColor` | Hex | Error state | ## Primary Button (Pay / Donate) | Key | Type | Description | | :- | :- | :- | | `buttonBackgroundColor` | Hex | Pay button background | | `buttonHoverColor` | Hex | Pay button hover | | `buttonBorderColor` | Hex | Pay button border | The **button label** (e.g. `Pay $10.00 + Tax`, `Donate $25.00`) is **not** a single `appearance` field—it is computed from donation mode, tax estimate, country, and tax-exempt flags. ## Payment Method Selector | Key | Type | Description | | :- | :- | :- | | `paymentMethodBorderColor` | Hex | Border around the payment method **container** and related inputs | | `paymentMethodButtonBackgroundColor` | Hex | Inactive method tab background | | `paymentMethodButtonHoverBackgroundColor` | Hex | Hover background | | `paymentMethodButtonActiveBackgroundColor` | Hex | Selected method tab background | | `paymentMethodButtonTextColor` | Hex | Method tab text | | `paymentMethodIconColor` | Hex | Icons on method tabs | ## Mobile Text & Input Overrides | Key | Type | Description | | :- | :- | :- | | `mobileHeadingTextColor` | Hex | Headings on mobile | | `mobileAmountTextColor` | Hex | Amount on mobile | | `mobileSecondaryTextColor` | Hex | Secondary text on mobile | | `mobileLabelTextColor` | Hex | Labels on mobile | | `mobileInputTextColor` | Hex | Input text on mobile | | `mobileInputBackgroundColor` | Hex | Input background on mobile | | `mobileInputBorderColor` | Hex | Input border on mobile | | `mobileButtonBackgroundColor` | Hex | Pay button on mobile | | `mobileButtonHoverColor` | Hex | Pay button hover on mobile | | `mobileFormBackgroundColor` | Hex | Form section on mobile | ## Cancel Button | Key | Type | Description | | :- | :- | :- | | `cancelButtonText` | String | Label (default: `Cancel and Return`) | | `cancelButtonBackgroundColor` | Hex | Background | | `cancelButtonTextColor` | Hex | Text color | | `cancelButtonHoverColor` | Hex | **Text** color on hover (not background) | | `cancelButtonBorderColor` | Hex | Border (`1px solid …` when set) | ## Branding | Key | Type | Description | | :- | :- | :- | | `logoUrl` | URL | Logo image (HTTPS) | | `logoPosition` | String | `inline` or `top` | | `logoMaxHeight` | String | In API; **not read** by live logo component | | `showMerchantName` | Boolean | Show name with logo (see [Customize Appearance](/guides/payments/checkout/appearance)) | | `hideBranding` | Boolean | In API; **not used** by the checkout page today | The **store name** comes from the required **`merchantName`** field when you create the session (stored on the session for the UI). ## Cart Display (Itemized Checkout) When using an `items` array: | Key | Type | Description | | :- | :- | :- | | `cartFontFamily` | String | Font for cart block (falls back to `fontFamily`) | | `cartProductNameColor` | Hex | Product name | | `cartBrandColor` | Hex | Brand line | | `cartDescriptionColor` | Hex | Description | | `cartVariantColor` | Hex | Variant line | | `cartCategoryColor` | Hex | Category | | `cartSkuColor` | Hex | SKU | | `cartPriceColor` | Hex | Price | | `cartOriginalPriceColor` | Hex | Strikethrough price | | `cartQuantityColor` | Hex | Quantity | | `cartDiscountBadgeColor` | Hex | Discount badge background | | `cartDiscountBadgeTextColor` | Hex | Discount badge text | | `cartTaxFreeColor` | Hex | Tax-free indicator | | `cartSubtotalColor` | Hex | Subtotal | | `cartTotalColor` | Hex | Total | | `cartTaxNoteColor` | Hex | Tax note text | Line content (images, badges, links) is controlled by each [cart item](/guides/payments/checkout/create-session) field, not `appearance`. ## API Warnings Session and payment-link creation may return **`warnings`** (not errors) when colors fail WCAG contrast checks or mobile/desktop combinations look mismatched. Surface these in your dashboard when present. ## Pre-Made Themes ### Minimal Light ```json theme={null} { "appearance": { "primaryColor": "#6366F1", "primaryHoverColor": "#4F46E5", "backgroundColor": "#FAFAFA", "textColor": "#1F2937", "fontFamily": "Poppins, sans-serif", "borderRadius": "lg" } } ``` ### Violet Dark Mode ```json theme={null} { "appearance": { "primaryColor": "#8b5cf6", "buttonBackgroundColor": "#8b5cf6", "leftSideBackground": "#0f0a1a", "rightSideBackground": "#1a0f2e", "formBackground": "#1a0f2e", "headingTextColor": "#ffffff", "textColor": "#e5e7eb", "inputBackgroundColor": "#1a0f2e", "inputBorderColor": "#8b5cf6", "cssVariables": { "page-background": "#0c0518" } } } ``` ### Emerald Clean ```json theme={null} { "appearance": { "primaryColor": "#10b981", "buttonBackgroundColor": "#10b981", "buttonHoverColor": "#059669", "inputFocusBorderColor": "#10b981", "borderRadius": "base" } } ``` ## Custom CSS | Key | Type | Description | | :- | :- | :- | | `customCss` | String | Raw CSS injected in a `

``` ### Backend (Express) ```js theme={null} import express from 'express'; import { Ozura, createSessionMiddleware } from '@ozura/elements/server'; const app = express(); app.use(express.json()); const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY }); // Session route — called automatically by the frontend SDK app.post('/api/oz-session', createSessionMiddleware(ozura)); // ACH route — receives the vault token and stores or forwards it app.post('/api/ach', async (req, res) => { const { token } = req.body; if (!token) return res.status(400).json({ error: 'token required' }); // Pass `token` to your ACH processor via the Vault Proxy, // or store it for future use. The token never expires server-side. // See: /guides/vault/proxy/overview console.log('Bank vault token:', token); res.json({ ok: true }); }); app.listen(3000); ``` *** ## Next Steps Forward the bank token to your ACH processor. Customize colors, fonts, and states. Use OzBankCard, OzBankAccountNumber, and OzBankRoutingNumber in React. Handle errors from createBankToken(). # Card Elements Source: https://docs.ozura.com/sdks/elements/card-elements Embed card number, expiry, and CVV fields and tokenize in the browser using OzVault. Card elements let you embed secure card input fields in your own page. Each field runs in a cross-origin iframe — raw card data never exists in your JavaScript or reaches your server. *** ## Field Types | `type` | Description | | - | - | | `cardNumber` | Full card number (13–19 digits) with Luhn validation, brand detection, and formatting (e.g. `xxxx xxxx xxxx xxxx` for 16-digit cards) | | `expirationDate` | `MM / YY` expiry with future-date validation | | `cvv` | 3-digit CVV (or 4-digit for Amex, auto-adjusts based on detected brand) | *** ## Creating and Mounting `OzVault.create()` is the async static factory — call it once when your checkout loads. It resolves once the session is initialized; mount elements immediately after. The vault is ready to tokenize once `onReady` fires (or `vault.isReady === true`). Gate your submit button on both vault readiness and field readiness. ```ts theme={null} import { OzVault, OzError } from '@ozura/elements'; let vault; try { vault = await OzVault.create({ pubKey: 'YOUR_PUB_KEY', // omit for test vault keys sessionUrl: '/api/oz-session', }); } catch (err) { // Pub key blocked, session endpoint failed, or backend unreachable console.error('Vault init failed:', err instanceof OzError ? err.message : err); // Show fallback UI — do not proceed return; } const cardNumber = vault.createElement('cardNumber'); const expiry = vault.createElement('expirationDate'); const cvv = vault.createElement('cvv'); // Mount by CSS selector or HTMLElement reference cardNumber.mount('#card-number'); expiry.mount('#expiry'); cvv.mount('#cvv'); ``` Each mounted field renders as a **46 px tall iframe**. The SDK sets that height automatically — you don't need to set a height on the wrapper `
`. Just make sure the wrapper has visible width and is not hidden with `overflow: hidden` or `height: 0`, or the field will be invisible. You can also pass element options at creation time: ```ts theme={null} const cardNumber = vault.createElement('cardNumber', { style: { base: { fontSize: '16px', color: '#1a1a2e' } }, placeholder: '1234 1234 1234 1234', }); ``` *** ## createElement Options ```ts theme={null} vault.createElement(type: ElementType, options?: ElementOptions): OzElement ``` | Option | Type | Description | | - | - | - | | `style` | `ElementStyleConfig` | Per-element style overrides. See [Styling](/sdks/elements/styling). | | `placeholder` | `string` | Placeholder text shown inside the field | | `disabled` | `boolean` | Mount the field in a disabled state | | `loadTimeoutMs` | `number` | Timeout (ms) before `loaderror` fires for this element. Default: `10000`. | *** ## Tokenizing Call `createToken()` after the user has filled all fields. The SDK transmits the field values directly to the Ozura Vault API — raw data never passes through your code. ```ts theme={null} document.getElementById('pay-btn').addEventListener('click', async (e) => { e.preventDefault(); try { const { token, cvcSession, card, billing } = await vault.createToken({ billing: { firstName: 'Jane', lastName: 'Smith', address: { line1: '123 Main St', city: 'San Francisco', state: 'CA', zip: '94102', country: 'US', }, }, }); // token — vault token, pass to your backend // cvcSession — CVC session ID; returned on success and required for cardSale // card.last4 — last 4 digits (safe to display) // card.brand — 'visa' | 'mastercard' | 'amex' | … // billing — validated, normalized billing ready for cardSale await fetch('/api/charge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, cvcSession, billing }), }); } catch (err) { if (err instanceof OzError) { console.error(err.message, err.errorCode); } } }); ``` ### createToken options ```ts theme={null} vault.createToken(options?: TokenizeOptions): Promise ``` | Option | Type | Description | | - | - | - | | `billing` | `BillingDetails` | Billing details to validate, normalize, and include in the token record | | `firstName` | `string` | *(deprecated)* Use `billing.firstName` instead | | `lastName` | `string` | *(deprecated)* Use `billing.lastName` instead | ### TokenResponse ```ts theme={null} { token: string; // vault token cvcSession: string; // CVC session ID — always present on success; pass to cardSale card?: { last4: string; // last 4 digits of card number brand: string; // 'visa' | 'mastercard' | 'amex' | 'discover' | … expMonth: string; // '01'–'12' expYear: string; // '2026', '2027', … }; billing?: BillingDetails; // echoed back (validated + normalized) if you passed billing } ``` **`cvcSession` is always present on a successful tokenization.** The SDK validates this field before resolving `createToken()` — if it is absent the promise rejects with an `OzError` (`errorCode: 'server'`), which would indicate a vault misconfiguration. Always forward both `token` and `cvcSession` to your charge endpoint: ```ts theme={null} const { token, cvcSession, billing } = await vault.createToken({ billing }); // cvcSession is guaranteed non-empty here await fetch('/api/charge', { method: 'POST', body: JSON.stringify({ token, cvcSession, billing }), }); ``` **`cvcSession` lifecycle.** The `cvcSession` is a short-lived vault credential that allows your server to retrieve the CVC value during a `cardSale` call. It shares the parent session TTL (default 30 minutes). It is safe to briefly persist it server-side — e.g. in a Redis key with a short TTL, or in a signed server-session — between the tokenization call and the charge call (seconds to minutes is fine). Do **not** store it long-term or log it. The vault invalidates the CVC credential after it is consumed by `cardSale`, so it is single-use. ### BillingDetails ```ts theme={null} { firstName: string; // required, 1–50 chars lastName: string; // required, 1–50 chars email?: string; // valid email, max 50 chars phone?: string; // E.164 format, e.g. '+15551234567' address?: { line1: string; // required line2?: string; // optional city: string; // required state: string; // required; US/CA normalized to 2-letter code zip: string; // required country: string; // ISO 3166-1 alpha-2, e.g. 'US' }; } ``` The SDK normalizes `state` to its standard 2-letter abbreviation for US and Canadian addresses. Full names (e.g. `"California"`, `"British Columbia"`) and 2-letter codes are both accepted. For `country: 'US'`, the state must be a valid US state, territory (PR, GU, VI, AS, MP), or military address code (AE, AP, AA) — other values are rejected. For `country: 'CA'`, the state must be a valid Canadian province code. **`cardSale` requires more billing fields than tokenize-only.** The OzuraPay API enforces `minLength: 1` on billing fields — passing an empty string `""` causes a validation error even if the field is technically optional. The SDK strips absent optional fields automatically (omits the key rather than sending `""`). However, in practice `cardSale` typically requires `email`, `phone`, and a full `address` to process the charge. If any of these are missing, the OzuraPay API will return a validation error. **Recommendation:** For charge flows, collect `email`, `phone`, and full address from the customer and pass them in `BillingDetails`. For tokenize-only flows (no charging), `billing` can be omitted entirely — or if you want billing details echoed back in `TokenResponse.billing`, pass at minimum `{ firstName, lastName }`. *** ## Events Each element emits events you can listen to with `.on()`: ```ts theme={null} cardNumber.on('change', (event) => { // event.empty — boolean: true when the field has no input // event.complete — boolean: field has reached the required length/format (may still be invalid — check `valid`) // event.valid — boolean: value passes full validation (e.g. Luhn check, expiry in future) // event.error — string | undefined: human-readable error message // event.cardBrand — string: 'visa' | 'mastercard' | 'amex' | … (cardNumber only) }); expiry.on('change', (event) => { // event.month — string: parsed month '01'–'12' (expirationDate only) // event.year — string: parsed 2-digit year e.g. '27' (expirationDate only) }); ``` **The expiry `onChange` event includes `month` and `year` values.** These are parsed from the masked expiry input and delivered to your handler for display purposes (e.g. showing the detected expiry date). If your PCI scope requires zero cardholder data on the merchant page, do not read or log these fields. ```ts theme={null} cardNumber.on('focus', () => { /* field received focus */ }); cardNumber.on('blur', (data) => { /* field lost focus; data = { empty, complete, valid, error } */ }); cardNumber.on('ready', () => { /* iframe loaded and is interactive */ }); cardNumber.on('loaderror', ({ elementType, error }) => { /* iframe failed to load */ }); ``` ### Gating the submit button The vault and each field iframe load independently. Gate your submit button on both: * **Vault readiness** — the `onReady` callback (fires when the vault is ready to tokenize) * **Field readiness** — each field's `'ready'` event (fires when the iframe loaded and is interactive) plus `change` events for completion ```ts theme={null} let vaultReady = false; const fieldReady = { cardNumber: false, expiry: false, cvv: false }; const fieldFilled = { cardNumber: false, expiry: false, cvv: false }; function updatePayButton() { const allReady = vaultReady && fieldReady.cardNumber && fieldReady.expiry && fieldReady.cvv && fieldFilled.cardNumber && fieldFilled.expiry && fieldFilled.cvv; document.getElementById('pay-btn').disabled = !allReady; } const vault = await OzVault.create({ pubKey: 'YOUR_PUB_KEY', // omit for test vault keys sessionUrl: '/api/oz-session', onReady: () => { vaultReady = true; updatePayButton(); }, }); cardNumber.on('ready', () => { fieldReady.cardNumber = true; updatePayButton(); }); expiry.on('ready', () => { fieldReady.expiry = true; updatePayButton(); }); cvv.on('ready', () => { fieldReady.cvv = true; updatePayButton(); }); cardNumber.on('change', ({ complete, valid }) => { fieldFilled.cardNumber = complete && valid; updatePayButton(); }); expiry.on('change', ({ complete, valid }) => { fieldFilled.expiry = complete && valid; updatePayButton(); }); cvv.on('change', ({ complete, valid }) => { fieldFilled.cvv = complete && valid; updatePayButton(); }); ``` *** ## Teardown When the checkout component unmounts, call `vault.destroy()` to remove all iframes and listeners. In vanilla JS use a cancel flag to handle the async nature of `OzVault.create()`: ```ts theme={null} let cancelled = false; let vault: OzVault | null = null; OzVault.create({ pubKey: 'YOUR_PUB_KEY', sessionUrl: '/api/oz-session', }).then(v => { if (cancelled) { v.destroy(); return; } vault = v; // mount elements... }); // On unmount: function cleanup() { cancelled = true; vault?.destroy(); } ``` *** ## Auto-Advance Focus Elements automatically advance focus from one field to the next on completion: * When **card number** is complete and valid → focus moves to **expiry** * When **expiry** is complete and valid → focus moves to **CVV** This is built-in and requires no configuration. *** ## Card Brand Detection The `cardNumber` element detects the card brand as the user types and emits it via `change`: ```ts theme={null} cardNumber.on('change', ({ cardBrand }) => { // Update your card brand icon document.getElementById('brand-icon').src = `/icons/${cardBrand}.svg`; }); ``` | Brand value | Description | | - | - | | `'visa'` | Visa | | `'mastercard'` | Mastercard | | `'amex'` | American Express (CVV becomes 4 digits) | | `'discover'` | Discover | | `'dinersclub'` | Diners Club | | `'jcb'` | JCB | | `'unknown'` | Not yet detected or unrecognized | *** ## Imperatively Managing Elements ```ts theme={null} // Focus a field programmatically cardNumber.focus(); // Clear a field cardNumber.clear(); // Check if the iframe is ready if (cardNumber.isReady) { /* safe to proceed */ } // Get the element type cardNumber.type; // 'cardNumber' // Tear down the iframe and remove listeners cardNumber.destroy(); ``` *** ## Full End-to-End Example (Vanilla JS + Express) **`onReady` can fire while `OzVault.create()` is still pending.** Vault initialization runs multiple steps in parallel. The `onReady` callback can fire while `await OzVault.create(...)` is still in progress — meaning `vault` is `undefined` inside the callback. Never reference `vault` inside `onReady`. Declare your readiness flags before calling `create()` so they exist when the callback runs. See the example below for the correct pattern. A complete working example: HTML page with card fields, Express backend with `/api/oz-session` and `/api/charge` routes, and the full tokenize → charge flow. ### Frontend (HTML + JS) ```html theme={null} Checkout

``` ### Backend (Express) ```js theme={null} // server.js import express from 'express'; import { Ozura, OzuraError, getClientIp, createSessionMiddleware } from '@ozura/elements/server'; const app = express(); app.use(express.json()); // Serve the SDK from the same origin for localhost CDN-free loading app.get('/oz-elements.esm.js', (_, res) => res.sendFile('node_modules/@ozura/elements/dist/oz-elements.esm.js', { root: '.' }) ); const ozura = new Ozura({ merchantId: process.env.MERCHANT_ID, apiKey: process.env.MERCHANT_API_KEY, vaultKey: process.env.VAULT_API_KEY, }); // Session route — called automatically by the frontend SDK app.post('/api/oz-session', createSessionMiddleware(ozura)); // Charge route — called after tokenization app.post('/api/charge', async (req, res) => { try { const { token, cvcSession, billing } = req.body; // Always fetch the authoritative amount from your own database const amount = '49.00'; const result = await ozura.cardSale({ token, cvcSession, amount, currency: 'USD', billing, clientIpAddress: getClientIp(req), }); // Store result.transactionId — required for refunds res.json({ success: true, transactionId: result.transactionId }); } catch (err) { const status = err instanceof OzuraError ? (err.statusCode || 500) : 500; const message = err instanceof OzuraError ? err.message : 'Internal server error'; res.status(status).json({ error: message }); } }); app.listen(3000, () => console.log('Listening on http://localhost:3000')); ``` This example serves the SDK locally to avoid CDN CORS issues on localhost. In production, use the CDN URL or your bundler — remove the `/oz-elements.esm.js` static route. See [Installation → CDN](/sdks/elements/installation#cdn-script-tag) for details. *** ## Test Cards These cards are for testing the **vault tokenization flow** — they are Luhn-valid and accepted by the vault in test mode. Use them to confirm your fields mount, tokenize, and return a token correctly. | Brand | Number | CVC | Expiry | | - | - | - | - | | Visa | `4111 1111 1111 1111` | Any 3 digits | Any future date | | Mastercard | `5555 5555 5555 4444` | Any 3 digits | Any future date | | Amex | `3782 822463 10005` | Any 4 digits | Any future date | **Testing a full charge with OzuraPay?** Use the cards on the [Test Credentials](/guides/test-credentials) page instead — those are calibrated for OzuraPay's sandbox, where the outcome (approve / decline) is determined by the charge amount, not the card number. **Testing your integration?** Use the test pub key on localhost — see [Installation → Local Development](/sdks/elements/installation#local-development) for setup details and test card numbers above. *** ## Next Steps Customize colors, fonts, and states. OzElements provider and pre-built components. Handle tokenization errors gracefully. Process the token with cardSale() on your backend. # Error Handling Source: https://docs.ozura.com/sdks/elements/error-handling Handle OzError instances from createToken() and createBankToken(), understand error codes, and surface errors to users. `createToken()` and `createBankToken()` both throw an `OzError` when tokenization fails. Wrap calls in a `try/catch` and inspect `.errorCode` to decide how to respond. *** ## OzError ```ts theme={null} import { OzError } from '@ozura/elements'; try { const { token } = await vault.createToken(); } catch (err) { if (err instanceof OzError) { console.error(err.message); // user-facing message console.error(err.errorCode); // machine-readable code console.error(err.raw); // raw message from vault (for logging) console.error(err.retryable); // boolean: safe to retry? } } ``` ### OzError properties | Property | Type | Description | | - | - | - | | `message` | `string` | Normalized, user-friendly error message | | `errorCode` | `OzErrorCode` | Machine-readable code (see table below) | | `raw` | `string` | Raw message returned by the Vault API (use for logging, not display) | | `retryable` | `boolean` | `true` for transient errors (`network`, `timeout`, `server`). `false` for permanent failures where retrying with the same input won't help. | *** ## Error Codes | `errorCode` | `retryable` | Meaning | What to do | | - | - | - | - | | `'network'` | ✅ | Request failed due to a network error (offline, DNS failure, etc.) | Show a "check your connection" message. Retry is safe. | | `'timeout'` | ✅ | Tokenization took longer than 30 seconds | Show a timeout message. Retry is safe. | | `'server'` | ✅ | Vault returned a 5xx error | Retry after a short delay. Log for investigation. | | `'auth'` | ❌ | Session key invalid/expired (after automatic refresh retry), or pub key misconfigured | If you see this in production after a successful initial load, the auto-refresh failed — check that your session endpoint (`/api/oz-session`) is reachable. Otherwise verify credentials. | | `'validation'` | ❌ | Request was malformed or the vault rejected input | Check request parameters. Usually a developer error. | | `'config'` | ❌ | Unexpected SDK or environment issue | Log the error and contact Ozura support. | | `'unknown'` | ❌ | Unclassified error | Log and surface a generic message. | **Session key expiry is handled automatically.** When a session key expires or is consumed between initialization and the user clicking Pay, the SDK silently fetches a fresh key and retries once. You will only receive an `auth` error if the refresh itself fails (e.g. your session endpoint is down or returns an error). The proactive refresh — triggered once `sessionLimit` successful submissions are reached — also prevents expiry from surfacing under normal usage. *** ## Recommended Pattern ```ts theme={null} import { OzVault, OzError } from '@ozura/elements'; async function handlePay() { try { const { token, cvcSession } = await vault.createToken({ billing: { firstName: 'Jane', lastName: 'Smith' }, }); await submitPayment(token, cvcSession); } catch (err) { if (!(err instanceof OzError)) throw err; // rethrow unexpected errors if (err.retryable) { showError('Connection issue — please try again.'); } else { switch (err.errorCode) { case 'auth': showError('Configuration error. Please contact support.'); console.error('[OzElements] Auth error:', err.raw); break; case 'validation': showError('Payment details could not be verified. Please check your card.'); break; case 'config': showError('Configuration error. Please contact support.'); console.error('[OzElements] Config error:', err.raw); break; default: showError('Something went wrong. Please try again.'); console.error('[OzElements] Unexpected error:', err); } } } } ``` *** ## SDK-Level Errors These `OzError` instances are thrown before any network request is made. They represent setup or usage mistakes caught during integration — fix them in code, don't surface them to the user. ### Initialization errors These come from `OzVault.create()`. If you're using the React provider, they appear in `initError`. | Condition | How to fix | | - | - | | `pubKey` missing | If using a **production** vault API key, pass a valid `pubKey` to `OzVault.create()` / ``. If using a test/sandbox vault API key, `pubKey` is not required and can be omitted. | | No session option provided | Pass `sessionUrl`, `getSessionKey`, or (deprecated) `fetchWaxKey` to `OzVault.create()` / ``. | | `sessionUrl` or `getSessionKey` returned an empty string | Check your `/api/oz-session` endpoint — it must return a non-empty `sessionKey`. | | Session fetch threw an error | Your session endpoint failed. Check server logs. | ### Tokenization errors These come from `createToken()` / `createBankToken()`. | Condition | How to fix | | - | - | | Vault not ready | Wait for the `onReady` callback (vanilla) or the `ready` flag (React) before submitting. | | No elements mounted | Call `mount()` on at least one element before tokenizing. | | `accountNumber` / `routingNumber` not ready (bank) | Mount both bank elements before calling `createBankToken()`. | | Tokenization already in progress | Disable your submit button while tokenization is pending. | | Missing `firstName` / `lastName` (bank) | Pass `firstName` and `lastName` in your `createBankToken()` options. | | Name exceeds 50 characters | Truncate or validate `firstName` / `lastName` before submitting. | | Invalid billing details | Validate `billing` fields before calling `createToken()`. Check the error message for the specific field. | | Tokenization timed out | `errorCode: 'timeout'` — retryable; show a timeout message. | ### Element errors | Condition | How to fix | | - | - | | `mount()` CSS selector not found | Verify the selector string targets an element that exists in the DOM at mount time. | | `mount()` passed `null` / `undefined` | Check your DOM reference before calling `mount()`. | These are developer errors — TypeScript types and the error messages above are the full diagnostic. *** ## Load Errors If the vault fails to load (network issue, blocked by CSP, etc.), use `onLoadError` in `OzVault.create()`: ```ts theme={null} const vault = await OzVault.create({ pubKey: 'YOUR_PUB_KEY', sessionUrl: '/api/oz-session', onLoadError: () => { showError('Payment fields could not load. Please refresh and try again.'); }, loadTimeoutMs: 8000, // default: 10000 }); ``` Individual element load failures emit a `loaderror` event with the element type and error message: ```ts theme={null} cardNumber.on('loaderror', ({ elementType, error }) => { console.error(`[OzElements] ${elementType} failed to load:`, error); showError('Card number field failed to load. Please refresh.'); }); ``` *** ## React: initError In React, `OzVault.create()` is called inside the `` provider. If it fails (e.g. your session endpoint is unreachable or returns an error), `useOzElements().initError` will be non-null: ```tsx theme={null} function CheckoutForm() { const { createToken, ready, initError } = useOzElements(); if (initError) { return

Payment unavailable — please refresh the page.

; } return (
); } ``` *** ## Error Normalization Helpers The SDK exports three standalone functions that map raw API error strings to user-friendly messages. `OzError.message` from `createToken()` and `createBankToken()` is **already normalized** — the SDK calls these functions internally. You only need to call them yourself when you receive raw error strings from a different source (e.g. your backend forwarding a vault or OzuraPay API response). ### normalizeVaultError Maps raw vault `/tokenize` error strings to user-facing messages for **card** flows. ```ts theme={null} import { normalizeVaultError } from '@ozura/elements'; const message = normalizeVaultError(rawVaultErrorString); // e.g. "Invalid card number" → "The card number is invalid. Please check and try again." ``` ### normalizeBankVaultError Maps raw vault `/tokenize` error strings to user-facing messages for **bank/ACH** flows. ```ts theme={null} import { normalizeBankVaultError } from '@ozura/elements'; const message = normalizeBankVaultError(rawVaultErrorString); // e.g. "Invalid routing number" → "The routing number is invalid. Please check and try again." ``` ### normalizeCardSaleError Maps raw OzuraPay API `cardSale` error strings to user-facing messages. Use this when your backend processes a charge via the OzuraPay API and needs to display the error to the user. ```ts theme={null} import { normalizeCardSaleError } from '@ozura/elements'; const message = normalizeCardSaleError(rawPayApiErrorString); // e.g. "Insufficient Funds" → "Your card has insufficient funds. Please use a different payment method." ``` All three functions fall back gracefully when no pattern matches: * **`normalizeVaultError`** and **`normalizeBankVaultError`** — return the original `raw` string unchanged when no pattern matches. * **`normalizeCardSaleError`** — short unrecognized strings (under 100 characters) are returned as-is; long opaque server strings (100+ characters) are replaced with `"Payment processing failed. Please try again or contact support."` An empty input returns `"Payment processing failed. Please try again."` *** ## Next Steps Full OzError type definition and all SDK types. createToken() full reference. # Installation Source: https://docs.ozura.com/sdks/elements/installation Add @ozura/elements to your project via npm, yarn, or a CDN script tag. ## npm / yarn ```bash npm theme={null} npm install @ozura/elements ``` ```bash yarn theme={null} yarn add @ozura/elements ``` **Pre-release builds (`@next`):** The `next` dist-tag contains unreleased changes before they reach `latest`. It is intended for Ozura-coordinated testing only — production credentials will not work with it. If you have been given access to pre-release testing, contact **[ammar@ozura.com](mailto:ammar@ozura.com)** for the matching staging credentials and setup instructions. ```bash theme={null} npm install @ozura/elements@next ``` `npm install @ozura/elements` (no tag) always resolves `latest` — `@next` has no impact on production installs. Then import in your JavaScript or TypeScript: ```ts theme={null} import { OzVault, OzError } from '@ozura/elements'; ``` For React components: ```ts theme={null} import { OzElements, useOzElements, OzCard, OzCardNumber, OzExpiry, OzCvv, OzBankCard, OzBankAccountNumber, OzBankRoutingNumber, } from '@ozura/elements/react'; ``` `@ozura/elements/react` requires **React 17 or later** as a peer dependency. If React is not already in your project, install it first: ```bash theme={null} npm install react react-dom ``` For server-side usage (Node.js, Deno, Bun): ```ts theme={null} import { Ozura, OzuraError, getClientIp, createCardSaleHandler, createSessionHandler } from '@ozura/elements/server'; ``` *** ## CDN (Script Tag) **The Ozura CDN blocks `localhost` origins.** If you load the CDN script on `http://localhost`, the browser will reject it with a CORS error (`No 'Access-Control-Allow-Origin' header`). The CDN is for deployed origins only. See [Local Development](#local-development) below. If you're not using a bundler, load the UMD build directly from the Ozura CDN: ```html theme={null} ``` Or use the ESM build with a native module script: ```html theme={null} ``` *** ## Local Development The CDN blocks `localhost`. For local development you have two options depending on how you load the SDK: ### npm install (recommended) If your project uses a bundler (webpack, Vite, Next.js, etc.), `@ozura/elements` is imported directly from `node_modules` — no CDN involved, no workaround needed. The iframes load from `elements.ozura.com` at runtime, but that uses `