Skip to main content

Documentation Index

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

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

@ozura/elements/server is the server-side counterpart to the Elements browser SDK. It wraps the Ozura Pay API and Vault API with typed methods, automatic field mapping, retry logic, and error handling.
import { Ozura, OzuraError, getClientIp, createSessionMiddleware } from '@ozura/elements/server';
Next.js / Fetch API users: use createSessionHandler instead of createSessionMiddleware. Both are exported from the same package. See createSessionHandler below.
ESM vs CJS: The package ships both ESM (import) and CommonJS (require) builds. Code examples throughout use ESM import syntax. If your project uses CommonJS (no "type": "module" in package.json, or a .cjs file), swap to require:
const { Ozura, OzuraError, getClientIp, createSessionMiddleware } =
  require('@ozura/elements/server');
Both builds are identical in behaviour — only the module format differs.
This module runs on your server (Node.js, Deno, Bun, etc.). Never import it in browser code — it requires your vault key and merchant API key, which must stay secret.

Quick Start

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

// Full integration (mint + tokenize + charge)
const ozura = new Ozura({
  merchantId: process.env.MERCHANT_ID!,
  apiKey:     process.env.MERCHANT_API_KEY!,
  vaultKey:   process.env.VAULT_API_KEY!,
});

// Charge a tokenized card (token comes from the frontend Elements SDK)
const result = await ozura.cardSale({
  token:           tokenResponse.token,
  cvcSession:      tokenResponse.cvcSession,
  amount:          '49.00',
  currency:        'USD',
  billing:         tokenResponse.billing,
  clientIpAddress: getClientIp(req),
});

console.log(result.transactionId);
Tokenize-only (no charging): merchantId and apiKey are not needed.
const ozura = new Ozura({ vaultKey: process.env.VAULT_API_KEY! });
// Use ozura.createSession() — cardSale() will throw if called without merchantId/apiKey

Configuration

new Ozura(config: OzuraConfig)
OptionTypeRequiredDefaultDescription
merchantIdstringCharge flows onlyYour Ozura merchant ID. Required for cardSale(). Omit for tokenize-only integrations.
apiKeystringCharge flows onlyMerchant Pay API key (sent as x-api-key on cardSale requests). Required for cardSale(). Omit for tokenize-only integrations.
vaultKeystringVault API secret key — used to create sessions. Never expose in browser code.
apiUrlstringOzura’s hosted API URL for the installed packagePay API base URL override
vaultUrlstringOzura’s hosted Vault URL for the installed packageVault API base URL override (for createSession and revokeSession)
timeoutMsnumber30000Request timeout in milliseconds
retriesnumber2Max retry attempts for 5xx / network errors on listTransactions and createSession. cardSale never retries.
Tokenize-only integrations (create sessions + tokenize cards, no charging) only need vaultKey. The merchantId and apiKey fields are validated lazily — they are only required when you call cardSale().
Store all credentials in environment variables. Never hardcode them in source code or commit them to version control.

Methods

cardSale

Charge a tokenized card. Maps the CardSaleInput shape to the Pay API’s flat field format.
ozura.cardSale(input: CardSaleInput): Promise<CardSaleResponseData>
cardSale never retries, regardless of the retries config. Financial POSTs are not idempotent — retrying a failed cardSale could cause a double charge. If you receive a 5xx response, check transaction records before retrying manually.
5xx de-duplication pattern. If cardSale throws with a 5xx status, the charge may or may not have been processed — the request could have timed out after the processor accepted it. Before retrying or failing the order, query listTransactions to check for a matching charge:
try {
  result = await ozura.cardSale({ token, cvcSession, amount, billing, clientIpAddress });
} catch (err) {
  if (err instanceof OzuraError && err.statusCode >= 500) {
    // Check whether a charge already landed
    const twoMinAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString().replace('T', ' ').slice(0, 19);
    const { transactions } = await ozura.listTransactions({
      dateFrom: twoMinAgo,
      transactionType: 'CreditCardSale',
    });
    // Match by amount and the last-4 returned in billing metadata
    const duplicate = transactions.find(tx =>
      tx.transactionType === 'CreditCardSale' &&
      tx.amount === amount
    );
    if (duplicate) {
      // Charge succeeded — use duplicate.transactionId
      result = duplicate;
    } else {
      throw err; // safe to surface to the user
    }
  } else {
    throw err;
  }
}
For stricter de-duplication, pass your own orderId in createSession when creating the session. You can filter listTransactions on a wider window and match by correlating your orderId to the transaction metadata.
Rate limit: 100 requests/minute per merchant.
const result = await ozura.cardSale({
  token:      tokenResponse.token,
  cvcSession: tokenResponse.cvcSession,
  amount:     '49.00',
  currency:   'USD',
  billing: {
    firstName: 'Jane',
    lastName:  'Smith',
    email:     'jane@example.com',
    phone:     '+15551234567',
    address: {
      line1:   '123 Main St',
      city:    'San Francisco',
      state:   'CA',
      zip:     '94102',
      country: 'US',
    },
  },
  clientIpAddress: getClientIp(req),
});

// Always store result.transactionId — required for refunds
console.log(result.transactionId, result.cardLastFour);
You can pass tokenResponse.billing directly from the frontend — the browser SDK validates and normalizes it for you.

listTransactions

List transactions by date range with pagination. Returns all transaction types.
ozura.listTransactions(input?: ListTransactionsInput): Promise<ListTransactionsResult>
Rate limit: 200 requests/minute per merchant.
// Look up a single transaction by ID
const { transactions } = await ozura.listTransactions({
  transactionId: 'txn_abc123',
});

// Or list by date range with pagination
const { transactions: txns, pagination } = await ozura.listTransactions({
  dateFrom:        '2025-01-01 00:00:00',
  dateTo:          '2025-12-31 23:59:59',
  transactionType: 'CreditCardSale',
  page:            1,
  limit:           25,
  sortBy:          'createdAt',
  sortOrder:       'desc',
});

if (pagination.hasNextPage) {
  // fetch next page with page + 1
}

createSession

Creates a short-lived payment session key from the vault. This is the server-side companion to sessionUrl / getSessionKey on the frontend SDK.
ozura.createSession(options?: CreateSessionOptions): Promise<CreateSessionResult>
OptionTypeRequiredDescription
sessionIdstringThe UUID the SDK generates and forwards through your session endpoint. Pass it here for vault-side correlation and audit.
sessionLimitnumber | nullMaximum card submissions this session accepts before the vault marks it consumed. Default: 3. Pass null to remove the cap (unlimited). Must match VaultOptions.sessionLimit on the client.
maxProxyCallsnumber | nullMaximum vault proxy calls. Omit for most integrations. Pass null to remove the cap.
orderIdstringYour order ID — stored on the session for support lookups and transaction correlation.
customerIdstringYour customer ID — stored on the session for support lookups.
cartIdstringYour cart/basket ID — stored on the session for support lookups.
metadataRecord<string, string>Arbitrary key/value pairs stored on the session. Max 16 keys, 64-char keys, 256-char values.
ttlSecondsnumberSession lifetime in seconds (60–1800). Default: 1800 (30 min). Shorten for flows where a tighter TTL is desirable.
Keep sessionLimit in sync with the client. Set the same value in VaultOptions.sessionLimit when calling OzVault.create() — see Installation → VaultOptions. A mismatch means the vault may reject a tokenize call before the client expects a refresh, causing a user-visible delay mid-checkout.
// Next.js App Router — manual implementation (or use createSessionHandler)
export async function POST(req: Request) {
  const { sessionId } = await req.json();
  const { sessionKey } = await ozura.createSession({
    sessionId,
    sessionLimit: 3,     // must match VaultOptions.sessionLimit on the client
    orderId:     order.id,   // optional — correlate Ozura records with your own DB
    customerId:  user.id,
    metadata:    { source: 'web' },
  });
  return Response.json({ sessionKey });
}
CreateSessionResult:
{
  sessionKey:       string;  // short-lived session credential
  expiresInSeconds: number;  // TTL, typically 1800 (30 minutes)
}

revokeSession

Revoke a payment session key. Best-effort — never throws. Call this on all three session-end paths to close the exposure window before the 30-minute vault TTL elapses. The SDK’s proactive/reactive refresh and sessionLimit are defence-in-depth layers — explicit revocation is the primary closure mechanism.
ozura.revokeSession(sessionKey: string): Promise<void>
Wire all three exit paths:
// 1. Payment success — revoke immediately after cardSale resolves
const result = await ozura.cardSale({ ... });
await ozura.revokeSession(sessionKey);  // session is spent; close the window immediately

// 2. User cancels checkout — revoke in your cancel route
// (store sessionKey server-side when the session is created — see pattern below)
app.post('/api/cancel', async (req, res) => {
  const { sessionKey } = await db.sessions.get(req.body.sessionId);
  await ozura.revokeSession(sessionKey);
  res.json({ ok: true });
});

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

Persisting session state for cancel/revoke

revokeSession(sessionKey) requires the sessionKey — but the browser SDK never exposes the session key to the page (it is kept inside the vault internals for security). To call revokeSession on cancel, your session route must store the mapping when the session is first created. The sessionId sent by the browser SDK to your session route is a UUID you can use as the lookup key. Store it server-side (e.g. Redis with the session TTL, a DB row, or an in-memory map for simple cases) when you create the session, then look it up when the cancel route fires.
// Session route — store sessionKey for later revocation
app.post('/api/oz-session', async (req, res) => {
  const { sessionId } = req.body;

  const { sessionKey, expiresInSeconds } = await ozura.createSession({ sessionId });

  // Store sessionId → sessionKey with a TTL matching the session
  await redis.setex(`session:${sessionId}`, expiresInSeconds, sessionKey);

  res.json({ sessionKey });
});

// Cancel route — look up and revoke
app.post('/api/cancel', async (req, res) => {
  const { sessionId } = req.body;
  if (!sessionId) return res.status(400).json({ error: 'sessionId required' });

  const sessionKey = await redis.get(`session:${sessionId}`);
  if (sessionKey) {
    await ozura.revokeSession(sessionKey);
    await redis.del(`session:${sessionId}`);
  }

  res.json({ ok: true });
});
If you do not need explicit revocation (e.g. tokenize-only flows with no cancel path), you can skip persistence entirely. Sessions expire automatically after their TTL (default 30 minutes). The sessionId the SDK sends is opaque from the browser’s perspective — it is safe to store as a key.

Session Route

The sessionUrl option in the browser SDK expects your backend to expose a POST /api/oz-session route. The SDK ships two factory functions to make this trivial.

createSessionHandler (Next.js / Fetch API)

Creates a handler for the Web Fetch API (RequestResponse). Use with Next.js App Router, Remix, Cloudflare Workers, etc.
import { Ozura, createSessionHandler } from '@ozura/elements/server';

const ozura = new Ozura({
  merchantId: process.env.MERCHANT_ID!,
  apiKey:     process.env.MERCHANT_API_KEY!,
  vaultKey:   process.env.VAULT_API_KEY!,
});

// Next.js App Router: app/api/oz-session/route.ts
export const POST = createSessionHandler(ozura);
The handler reads sessionId from the JSON body, calls ozura.createSession(), and returns { sessionKey }.

createSessionMiddleware (Express / Connect)

Creates an Express-style middleware ((req, res) => void). Requires express.json() (or equivalent body-parser) to be registered before it.
import express from 'express';
import { Ozura, createSessionMiddleware } from '@ozura/elements/server';

const ozura = new Ozura({ /* ... */ });

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

Card Sale Handler Factories

For backends where a dedicated route fully owns the card sale flow (amount from server-side DB, billing from token, IP from request), the SDK ships factory functions that build complete handlers.

createCardSaleHandler (Next.js / Fetch API)

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

const ozura = new Ozura({ /* ... */ });

export const POST = createCardSaleHandler(ozura, {
  getAmount: async (body) => {
    // Return the authoritative amount from YOUR database.
    // Never trust the client-submitted amount.
    const order = await db.orders.findById(body.orderId as string);
    return order.total; // e.g. "49.00"
  },
});

createCardSaleMiddleware (Express / Connect)

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

const ozura = new Ozura({ /* ... */ });

app.post('/api/charge', createCardSaleMiddleware(ozura, {
  getAmount: async (body) => {
    const order = await db.orders.findById(body.orderId as string);
    return order.total;
  },
}));
Both factories accept the same options:
OptionTypeRequiredDescription
getAmount(body) => string | Promise<string>Return the authoritative transaction amount as a decimal string (e.g. "49.00"). Source this from your own database — never trust the value the client sends.
getCurrency(body) => string | Promise<string>Return the ISO 4217 currency code (e.g. "USD", "CAD"). Defaults to "USD".
Both factories also:
  • Read token, cvcSession, and billing from the request body
  • Reject non-POST methods (405) and non-JSON content types (415)
  • Call getClientIp() to resolve the client IP
  • On success: return { transactionId, amount, cardLastFour, cardBrand }
  • On Pay API error: return { error: string } with the normalized error message and the appropriate HTTP status (4xx/5xx); 429 includes a Retry-After header

Utilities

getClientIp

Extract the client IP address from a server request object. Works across frameworks:
import { getClientIp } from '@ozura/elements/server';
FrameworkHow it reads IP
Express / Fastifyreq.ip (requires app.set('trust proxy', true) behind a load balancer)
Next.js App Routercf-connecting-ipx-forwarded-forx-real-ip
Raw Node.jscf-connecting-ipx-forwarded-forx-real-ipsocket.remoteAddress
// Express
app.post('/api/charge', async (req, res) => {
  const result = await ozura.cardSale({
    // ...
    clientIpAddress: getClientIp(req),
  });
});
Always fetch the client IP on the server. Never trust a value sent from the browser — ad blockers and browser extensions can interfere with client-side IP detection services.Headers like x-forwarded-for and x-real-ip are only trustworthy when your server sits behind a reverse proxy that strips and rewrites them. If your Node.js process is directly internet-accessible, an attacker can spoof these values.
What happens if clientIpAddress is wrong or missing? The Pay API accepts the field without strict validation — it will not reject a cardSale request because the IP is an internal address, 127.0.0.1, or appears blank. The IP is used for fraud scoring and audit logging; an incorrect value weakens fraud detection but does not block the transaction. If getClientIp cannot resolve a real IP (e.g. headers are absent or your proxy uses a non-standard header), the charge will still proceed — but your fraud risk posture degrades. If you are behind a proxy that uses a custom header, extract the IP manually and pass it as a string rather than relying on getClientIp.

Error Handling

All methods throw OzuraError on failure.
import { Ozura, OzuraError } from '@ozura/elements/server';

try {
  const result = await ozura.cardSale({ /* ... */ });
} catch (err) {
  if (err instanceof OzuraError) {
    console.error(err.message);      // user-facing message
    console.error(err.statusCode);   // HTTP status (0 for network/timeout)
    console.error(err.raw);          // raw API error string
    console.error(err.retryAfter);   // seconds (429 only)
  }
}

OzuraError

class OzuraError extends Error {
  readonly statusCode:  number;   // HTTP status code (0 for network/timeout errors)
  readonly raw:         string;   // raw API error message
  readonly retryAfter?: number;   // retry-after seconds (429 responses only)
}

Retry behavior

listTransactions and createSession automatically retry on 5xx and network errors with exponential backoff (1 s, 2 s, 4 s…). 4xx errors are never retried. cardSale is never retried — see note above.
StatusRetried (listTransactions)?createSessioncardSale
5xx✅ Up to retries times✅ Up to retries times, exponential backoff❌ Never retried
Network error✅ Up to retries times✅ Up to retries times, exponential backoff❌ Never retried
4xx
429❌ Throw with retryAfter

Types

CreateSessionOptions

type CreateSessionOptions = {
  sessionId?:    string;           // SDK-generated session UUID forwarded by your endpoint
  sessionLimit?: number | null;    // default 3; null = no cap; must match VaultOptions.sessionLimit
  maxProxyCalls?: number | null;   // vault proxy call limit; omit for most integrations
  orderId?:      string;           // your order ID — stored for transaction correlation
  customerId?:   string;           // your customer ID — stored for transaction correlation
  cartId?:       string;           // your cart ID — stored for transaction correlation
  metadata?:     Record<string, string>; // max 16 keys, 64-char keys, 256-char values
  ttlSeconds?:   number;           // session TTL 60–1800s; default 1800 (30 min)
};

CreateSessionResult

type CreateSessionResult = {
  sessionKey:       string;  // short-lived session credential
  expiresInSeconds: number;  // TTL, typically 1800 (30 minutes)
};
The deprecated MintWaxKeyOptions and MintWaxKeyResult types are still exported as aliases to the above for backward compatibility.

CardSaleInput

type CardSaleInput = {
  token:             string;            // from TokenResponse.token
  cvcSession:        string;            // from TokenResponse.cvcSession
  amount:            string;            // decimal string, e.g. "49.00"
  currency?:         string;            // ISO 4217, default "USD"
  billing:           BillingDetails;
  clientIpAddress:   string;            // use getClientIp()
  salesTaxExempt?:   boolean;           // default false
  processor?:        'elavon' | 'nuvei' | 'worldpay';
  surchargePercent?: string;            // default "0.00", range 0–3
  tipAmount?:        string;            // default "0.00"
};

CardSaleResponseData

type CardSaleResponseData = {
  transactionId:    string;   // store this — required for refunds
  amount:           string;
  currency:         string;
  surchargeAmount?: string;   // present only when non-zero
  salesTaxAmount?:  string;
  tipAmount?:       string;   // present only when non-zero
  cardLastFour:     string;
  cardBin?:         string;
  cardExpMonth:     string;
  cardExpYear:      string;
  cardBrand:        string;
  isCreditCard?:    boolean;
  transDate:        string;   // ISO 8601
  ozuraMerchantId:  string;
  clientIpAddress?: string;
  billingFirstName: string;
  billingLastName:  string;
  billingEmail:     string;
  billingPhone:     string;
  billingAddress1:  string;
  billingAddress2?: string;
  billingCity:      string;
  billingState:     string;
  billingZipcode:   string;
  billingCountry:   string;
};
surchargeAmount and tipAmount are only present in the response when non-zero. Handle missing values defensively:
const surcharge = result.surchargeAmount ?? '0.00';
const tip       = result.tipAmount       ?? '0.00';

ListTransactionsInput

type ListTransactionsInput = {
  transactionId?:   string;             // look up a single transaction by ID
  dateFrom?:        string;             // "YYYY-MM-DD HH:MM:SS"
  dateTo?:          string;             // "YYYY-MM-DD HH:MM:SS"
  transactionType?: TransactionType;
  page?:            number;             // 1-based, default 1
  limit?:           number;             // default 50, max 100
  fields?:          string;             // comma-separated field names
  sortBy?:          string;
  sortOrder?:       'asc' | 'desc';
};

Transaction Types

TransactionType

type TransactionType =
  | 'CreditCardSale' | 'CreditCardReversal' | 'CreditCardReturn' | 'CreditCardVoid'
  | 'AchSale'        | 'AchReversal'        | 'AchReturn'        | 'AchVoid'
  | 'CryptoSale'     | 'CryptoReturn'       | 'CryptoWithdrawal';

Narrowing TransactionData

listTransactions() returns TransactionData — a discriminated union. Use transactionType to narrow:
const { transactions } = await ozura.listTransactions({ transactionId: 'txn_abc123' });
const tx = transactions[0];

if (tx.transactionType === 'CreditCardSale') {
  console.log(tx.cardLastFour, tx.cardBrand);
} else if (tx.transactionType === 'AchSale') {
  console.log(tx.routingNumber);
}
Field name difference: cardSale() returns billing fields as billingFirstName, billingLastName, etc. Transaction queries return them as firstName, lastName, etc. The SDK types reflect what each endpoint actually returns.

Full Example

Express backend handling a card payment end-to-end:
import express from 'express';
import { Ozura, OzuraError, getClientIp, createSessionMiddleware } from '@ozura/elements/server';

const app = express();
app.use(express.json());

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 Elements SDK
app.post('/api/oz-session', createSessionMiddleware(ozura));

// Card charge route
app.post('/api/charge', async (req, res) => {
  try {
    const { token, cvcSession, billing } = req.body;

    // Fetch authoritative amount from your DB — never trust the client
    const order = await db.orders.findById(req.body.orderId);

    const result = await ozura.cardSale({
      token,
      cvcSession,
      amount:          order.total,
      currency:        'USD',
      billing,
      clientIpAddress: getClientIp(req),
    });

    res.json({
      success:       true,
      transactionId: result.transactionId,
      last4:         result.cardLastFour,
    });
  } catch (err) {
    if (err instanceof OzuraError) {
      res.status(err.statusCode || 500).json({ error: err.message });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

app.listen(3000);

Next Steps

Card Elements

Set up the frontend card fields that produce the tokens this SDK consumes.

Error Handling

Normalize vault and payment errors for your users.

API Reference

Full type definitions for all SDK exports.

Installation

Credentials and OzVault.create() setup.