Skip to main content

Server SDK

@ozura/elements/server is the server-side counterpart to the Elements browser SDK. It wraps the Ozura Pay API with typed methods, automatic field mapping, retry logic, and error handling — so your backend can process payments using the tokens created by Elements.
import { Ozura } from '@ozura/elements/server';
This module runs on your server (Node.js, Deno, Bun, etc.). Never import it in browser code — it requires your merchant API key and vault key, which must stay secret.

Quick Start

import { Ozura } 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!,
});

// 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: req.ip,
});

console.log(result.transactionId);

Configuration

new Ozura(config: OzuraConfig)
OptionTypeRequiredDefaultDescription
merchantIdstringYour Ozura merchant ID
apiKeystringMerchant Pay API key (sent as x-api-key header)
vaultKeystringVault API key — same key used in OzVault on the frontend
apiUrlstringProduction URLPay API base URL override
timeoutMsnumber30000Request timeout in milliseconds
retriesnumber2Max retry attempts for 5xx / network errors. Set 0 to disable.
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.
const result = await ozura.cardSale(input: CardSaleInput): Promise<CardSaleResponseData>
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),
});

// Store result.transactionId — required for refunds
You can pass tokenResponse.billing directly from the frontend — the browser SDK validates and normalizes it for you.

getTransaction

Retrieve a single transaction by ID. Returns any transaction type (card, ACH, or crypto).
const tx = await ozura.getTransaction(transactionId: string): Promise<TransactionData>
Rate limit: 100 requests/minute per merchant.
const tx = await ozura.getTransaction('txn_abc123');

// Narrow by transaction type to access type-specific fields
if (tx.transactionType === 'CreditCardSale') {
  console.log(tx.cardLastFour, tx.cardBrand);
}
Known issue: The Pay API requires the API key as a query parameter (access_token) for this endpoint. This means it can appear in server logs and CDN caches. The SDK will switch to header-based auth once the backend supports it.

listTransactions

List transactions by date range with pagination. Returns all transaction types.
const result = await ozura.listTransactions(input?: ListTransactionsInput): Promise<ListTransactionsResult>
Rate limit: 200 requests/minute per merchant.
const { transactions, 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',
});

for (const tx of transactions) {
  console.log(tx.transactionId, tx.amount, tx.status);
}

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

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
Next.js App Routerreq.headers.get('x-forwarded-for')
Raw Node.jsreq.headers['x-forwarded-for'] or req.socket.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.

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

The SDK automatically retries on 5xx and network errors with exponential backoff (1s, 2s, 4s, up to 8s). 4xx errors — including 429 rate limit — are never retried automatically.
StatusRetried?Action
5xx✅ Up to retries timesAutomatic
Network error✅ Up to retries timesAutomatic
Timeout✅ Up to retries timesAutomatic
4xxThrow immediately
429Throw with retryAfter

Types

CardSaleInput

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

CardSaleResponseData

Returned by cardSale(). This is the response from the Pay API POST /cardSale endpoint.
type CardSaleResponseData = {
  transactionId:    string;
  amount:           string;
  currency:         string;
  surchargeAmount:  string;
  salesTaxAmount?:  string;
  tipAmount:        string;
  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;
};

ListTransactionsInput

type ListTransactionsInput = {
  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';
};

ListTransactionsResult

type ListTransactionsResult = {
  transactions: TransactionData[];
  pagination:   TransactionQueryPagination;
};

TransactionQueryPagination

type TransactionQueryPagination = {
  currentPage: number;
  totalPages:  number;
  totalCount:  number;
  limit:       number;
  hasNextPage: boolean;
  hasPrevPage: boolean;
  nextPage:    number | null;
  prevPage:    number | null;
};

Transaction Types

The getTransaction() and listTransactions() methods return TransactionData — a discriminated union covering all transaction types. Use transactionType to narrow and access type-specific fields.

TransactionType

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

TransactionData (discriminated union)

type TransactionData = CardTransactionData | AchTransactionData | CryptoTransactionData;
Narrow via transactionType:
const tx = await ozura.getTransaction('txn_abc123');

switch (tx.transactionType) {
  case 'CreditCardSale':
  case 'CreditCardReturn':
  case 'CreditCardReversal':
  case 'CreditCardVoid':
    console.log(tx.cardLastFour, tx.cardBrand);
    break;
  case 'AchSale':
  case 'AchReturn':
  case 'AchReversal':
  case 'AchVoid':
    console.log(tx.routingNumber);
    break;
  case 'CryptoSale':
  case 'CryptoReturn':
  case 'CryptoWithdrawal':
    console.log(tx.network, tx.originalSourceAddress);
    break;
}

Shared fields (TransactionBase)

All transaction types share these fields:
FieldTypeDescription
transactionIdstringUnique transaction reference
transactionTypeTransactionTypeDiscriminant for narrowing
amountstringTransaction amount
currentAmountstring?Current amount after partial refunds
currencystringISO 4217 currency code
statusstringTransaction status
createdAtstringISO 8601 timestamp
surchargeAmountstring?Surcharge applied
tipAmountstring?Tip amount
originalTransactionIdstring?Parent transaction (for refunds, reversals)
relatedTransactionIdsstring[]?Linked transactions
isVoidedboolean?Whether voided
isPartiallyReturnedboolean?Partial refund applied
isFullyReturnedboolean?Full refund applied
isReversedboolean?Whether reversed
clientIpAddressstring?Client IP recorded at transaction time
ozuraUserDetailsobject?Contains ozuraUserId and ozuraMerchantId
avsResponseCodestring?Address Verification System response code from processor
cvvResponseCodestring?CVV verification response code from processor
salesTaxDataobject?Sales tax calculation details
firstNamestring?Customer first name
lastNamestring?Customer last name
emailstring?Customer email
phonestring?Customer phone
address1string?Billing address line 1
address2string?Billing address line 2
citystring?Billing city
statestring?Billing state
zipcodestring?Billing zip code
countrystring?Billing country
Field name difference: cardSale() returns billing fields as billingFirstName, billingLastName, etc. Transaction queries return them as firstName, lastName, etc. The types reflect what each endpoint actually returns.

CardTransactionData

Additional fields when transactionType is a card type:
FieldTypeDescription
cardLastFourstring?Last 4 digits
cardBinstring?First 6 digits (BIN)
cardExpMonthstring?Expiry month
cardExpYearstring?Expiry year
cardBrandstring?'visa', 'mastercard', 'amex', etc.
isCreditCardboolean?Credit vs debit

AchTransactionData

Additional fields when transactionType is an ACH type:
FieldTypeDescription
ddaAccountTypestring?Account type
accountNumberstring?Account number
routingNumberstring?Routing number

CryptoTransactionData

Additional fields when transactionType is a crypto type:
FieldTypeDescription
originalSourceAddressstring?Source wallet address
originalDepositAddressstring?Deposit wallet address
networkstring?Blockchain network
mintDetailsobject?Contains mintTransactionHash

Full Example

Express backend handling a card payment end-to-end:
import express from 'express';
import { Ozura, OzuraError, getClientIp } 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!,
});

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

    const result = await ozura.cardSale({
      token,
      cvcSession,
      amount:          '49.00',
      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.get('/api/transactions', async (req, res) => {
  try {
    const { transactions, pagination } = await ozura.listTransactions({
      dateFrom: req.query.from as string,
      dateTo:   req.query.to as string,
      page:     Number(req.query.page) || 1,
      limit:    25,
    });

    res.json({ transactions, pagination });
  } 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