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)
| Option | Type | Required | Default | Description |
|---|
merchantId | string | ✅ | — | Your Ozura merchant ID |
apiKey | string | ✅ | — | Merchant Pay API key (sent as x-api-key header) |
vaultKey | string | ✅ | — | Vault API key — same key used in OzVault on the frontend |
apiUrl | string | — | Production URL | Pay API base URL override |
timeoutMs | number | — | 30000 | Request timeout in milliseconds |
retries | number | — | 2 | Max 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';
| Framework | How it reads IP |
|---|
| Express / Fastify | req.ip |
| Next.js App Router | req.headers.get('x-forwarded-for') |
| Raw Node.js | req.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.
| Status | Retried? | Action |
|---|
| 5xx | ✅ Up to retries times | Automatic |
| Network error | ✅ Up to retries times | Automatic |
| Timeout | ✅ Up to retries times | Automatic |
| 4xx | ❌ | Throw immediately |
| 429 | ❌ | Throw with retryAfter |
Types
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;
};
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;
};
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:
| Field | Type | Description |
|---|
transactionId | string | Unique transaction reference |
transactionType | TransactionType | Discriminant for narrowing |
amount | string | Transaction amount |
currentAmount | string? | Current amount after partial refunds |
currency | string | ISO 4217 currency code |
status | string | Transaction status |
createdAt | string | ISO 8601 timestamp |
surchargeAmount | string? | Surcharge applied |
tipAmount | string? | Tip amount |
originalTransactionId | string? | Parent transaction (for refunds, reversals) |
relatedTransactionIds | string[]? | Linked transactions |
isVoided | boolean? | Whether voided |
isPartiallyReturned | boolean? | Partial refund applied |
isFullyReturned | boolean? | Full refund applied |
isReversed | boolean? | Whether reversed |
clientIpAddress | string? | Client IP recorded at transaction time |
ozuraUserDetails | object? | Contains ozuraUserId and ozuraMerchantId |
avsResponseCode | string? | Address Verification System response code from processor |
cvvResponseCode | string? | CVV verification response code from processor |
salesTaxData | object? | Sales tax calculation details |
firstName | string? | Customer first name |
lastName | string? | Customer last name |
email | string? | Customer email |
phone | string? | Customer phone |
address1 | string? | Billing address line 1 |
address2 | string? | Billing address line 2 |
city | string? | Billing city |
state | string? | Billing state |
zipcode | string? | Billing zip code |
country | string? | 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:
| Field | Type | Description |
|---|
cardLastFour | string? | Last 4 digits |
cardBin | string? | First 6 digits (BIN) |
cardExpMonth | string? | Expiry month |
cardExpYear | string? | Expiry year |
cardBrand | string? | 'visa', 'mastercard', 'amex', etc. |
isCreditCard | boolean? | Credit vs debit |
AchTransactionData
Additional fields when transactionType is an ACH type:
| Field | Type | Description |
|---|
ddaAccountType | string? | Account type |
accountNumber | string? | Account number |
routingNumber | string? | Routing number |
CryptoTransactionData
Additional fields when transactionType is a crypto type:
| Field | Type | Description |
|---|
originalSourceAddress | string? | Source wallet address |
originalDepositAddress | string? | Deposit wallet address |
network | string? | Blockchain network |
mintDetails | object? | 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