Skip to main content

API Best Practices

Best practices for building reliable, secure, and performant applications with the Torvus API.


Authentication & Security

API Key Management

Never Hardcode API Keys:

Bad:

const TORVUS_API_KEY = 'tvs_live_abc123def456...'; // Hardcoded!

fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Authorization': `Bearer ${TORVUS_API_KEY}`
}
});

Good:

// Use environment variables
const TORVUS_API_KEY = process.env.TORVUS_API_KEY;

fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Authorization': `Bearer ${TORVUS_API_KEY}`
}
});

Environment Variables:

# .env (never commit this file!)
TORVUS_API_KEY=tvs_live_abc123def456...

# .env.example (commit this)
TORVUS_API_KEY=your_api_key_here

Configuration Files:

// config.js
export const config = {
torvus: {
apiKey: process.env.TORVUS_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
}
};

Key Rotation

Rotate Keys Regularly:

  • Rotate production keys every 90 days
  • Rotate immediately if key is compromised
  • Use different keys for development, staging, and production

Rotation Process:

  1. Generate new API key in Torvus dashboard
  2. Update environment variable in deployment system
  3. Deploy application with new key
  4. Verify new key works
  5. Revoke old key after 24-hour grace period

Multiple Keys (recommended):

// Use primary + fallback key for zero-downtime rotation
const apiKeys = [
process.env.TORVUS_API_KEY_PRIMARY,
process.env.TORVUS_API_KEY_FALLBACK
];

async function callTorvusAPI(endpoint, options) {
for (const key of apiKeys) {
try {
const response = await fetch(`${baseUrl}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${key}`,
...options.headers
}
});

if (response.status === 401) {
continue; // Try next key
}

return response;
} catch (error) {
// Log error, try next key
}
}

throw new Error('All API keys failed');
}

Secure Storage

Backend Applications:

  • Store keys in environment variables or secrets manager
  • Use AWS Secrets Manager, Google Cloud Secret Manager, HashiCorp Vault, etc.
  • Never log API keys (even in debug mode)

Frontend Applications:

  • Never use API keys in frontend code (visible to users)
  • ✅ Use backend proxy to make API calls
  • ✅ Use session tokens with short expiration

Example Backend Proxy:

// Backend API endpoint (Node.js/Express)
app.post('/api/vaults', async (req, res) => {
// Verify user session first
if (!req.session.userId) {
return res.status(401).json({ error: 'Unauthorized' });
}

// Call Torvus API with backend API key (never exposed to frontend)
const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.TORVUS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(req.body)
});

const data = await response.json();
res.json(data);
});

// Frontend calls backend proxy (no API key exposed)
fetch('/api/vaults', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'My Vault' })
});

Error Handling

Comprehensive Error Handling

Handle All Error Types:

async function createVault(vaultData) {
try {
const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(vaultData)
});

// Handle HTTP errors
if (!response.ok) {
const error = await response.json();

switch (response.status) {
case 400:
throw new ValidationError(error.message, error.errors);
case 401:
throw new AuthenticationError('Invalid API key');
case 403:
throw new PermissionError('Insufficient permissions');
case 404:
throw new NotFoundError('Resource not found');
case 429:
throw new RateLimitError('Rate limit exceeded', error.retry_after);
case 500:
case 502:
case 503:
case 504:
throw new ServerError('Server error, please retry');
default:
throw new APIError(`Unexpected error: ${response.status}`);
}
}

return await response.json();

} catch (error) {
// Handle network errors
if (error instanceof TypeError && error.message === 'Failed to fetch') {
throw new NetworkError('Network error, check connection');
}

// Handle timeout errors
if (error.name === 'AbortError') {
throw new TimeoutError('Request timeout');
}

// Re-throw API errors
throw error;
}
}

// Custom error classes
class ValidationError extends Error {
constructor(message, errors) {
super(message);
this.name = 'ValidationError';
this.errors = errors; // Detailed validation errors
}
}

class RateLimitError extends Error {
constructor(message, retryAfter) {
super(message);
this.name = 'RateLimitError';
this.retryAfter = retryAfter; // Seconds to wait
}
}

User-Friendly Error Messages

Don't Expose Technical Details to Users:

Bad:

catch (error) {
alert(error.message); // "Invalid API key" - confusing to user
}

Good:

catch (error) {
if (error instanceof ValidationError) {
showFormErrors(error.errors);
} else if (error instanceof RateLimitError) {
showMessage('Too many requests. Please try again in a few minutes.');
} else if (error instanceof NetworkError) {
showMessage('Connection error. Please check your internet.');
} else {
showMessage('Something went wrong. Please try again.');
logError(error); // Log for debugging
}
}

Retry Logic

Implement Exponential Backoff:

async function callAPIWithRetry(apiCall, maxRetries = 3) {
let lastError;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiCall();
} catch (error) {
lastError = error;

// Don't retry client errors (4xx except 429)
if (error.status >= 400 && error.status < 500 && error.status !== 429) {
throw error;
}

// Calculate backoff delay: 2^attempt * 1000ms (1s, 2s, 4s...)
const delay = Math.pow(2, attempt) * 1000;

// Add jitter to prevent thundering herd
const jitter = Math.random() * 1000;
const totalDelay = delay + jitter;

console.log(`Retry attempt ${attempt + 1} after ${totalDelay}ms`);
await sleep(totalDelay);
}
}

throw lastError;
}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
const vault = await callAPIWithRetry(() => createVault(vaultData));

Respect Retry-After Header (for 429 errors):

async function handleRateLimit(response) {
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitSeconds = retryAfter ? parseInt(retryAfter) : 60;

console.log(`Rate limited. Waiting ${waitSeconds} seconds...`);
await sleep(waitSeconds * 1000);

// Retry request
return await fetch(/* same request */);
}
}

Rate Limiting

Respect Rate Limits

Check Headers:

async function trackRateLimit(response) {
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');

console.log(`Rate limit: ${remaining}/${limit} remaining`);
console.log(`Resets at: ${new Date(reset * 1000).toISOString()}`);

// Warn if approaching limit
if (remaining < limit * 0.1) {
console.warn('⚠️ Approaching rate limit!');
}
}

Request Queuing

Queue Requests to Stay Under Limit:

class RateLimitedQueue {
constructor(requestsPerSecond) {
this.requestsPerSecond = requestsPerSecond;
this.queue = [];
this.processing = false;
}

async enqueue(apiCall) {
return new Promise((resolve, reject) => {
this.queue.push({ apiCall, resolve, reject });
this.process();
});
}

async process() {
if (this.processing || this.queue.length === 0) {
return;
}

this.processing = true;

while (this.queue.length > 0) {
const { apiCall, resolve, reject } = this.queue.shift();

try {
const result = await apiCall();
resolve(result);
} catch (error) {
reject(error);
}

// Wait between requests (1000ms / requestsPerSecond)
const delay = 1000 / this.requestsPerSecond;
await sleep(delay);
}

this.processing = false;
}
}

// Usage
const queue = new RateLimitedQueue(10); // 10 requests/second

// All requests are automatically queued and rate-limited
const vault1 = await queue.enqueue(() => createVault(data1));
const vault2 = await queue.enqueue(() => createVault(data2));
const vault3 = await queue.enqueue(() => createVault(data3));

Batch Operations

Use Batch Endpoints When Available:

Bad (1000 API calls):

for (const recipient of recipients) {
await createRecipient(vaultId, recipient); // 1 API call each
}

Good (1 API call):

// Batch create recipients (if endpoint exists)
await createRecipientsBatch(vaultId, recipients);

// Or use bulk upload endpoint
await uploadRecipientCSV(vaultId, csvData);

Implement Client-Side Batching (if API doesn't support batching):

async function batchedCreate(items, batchSize = 10) {
const results = [];

for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);

// Process batch concurrently
const batchResults = await Promise.all(
batch.map(item => createItem(item))
);

results.push(...batchResults);

// Optional: delay between batches
if (i + batchSize < items.length) {
await sleep(100);
}
}

return results;
}

Performance Optimization

Connection Reuse

Use HTTP Keep-Alive:

// Node.js - reuse connections
const https = require('https');

const agent = new https.Agent({
keepAlive: true,
maxSockets: 10
});

fetch('https://api.torvussecurity.com/v1/vaults', {
agent: agent
});

Use HTTP/2 (if supported):

// Modern fetch automatically uses HTTP/2 when available
const response = await fetch('https://api.torvussecurity.com/v1/vaults');

Concurrent Requests

Parallelize Independent Requests:

Bad (sequential, slow):

const vaults = await getVaults();
const documents = await getDocuments();
const recipients = await getRecipients();
// Total time: 3 * request time

Good (parallel, fast):

const [vaults, documents, recipients] = await Promise.all([
getVaults(),
getDocuments(),
getRecipients()
]);
// Total time: 1 * request time

Limit Concurrency (to avoid overwhelming server):

async function parallelWithLimit(items, limit, fn) {
const results = [];
const executing = [];

for (const item of items) {
const promise = fn(item).then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});

results.push(promise);
executing.push(promise);

if (executing.length >= limit) {
await Promise.race(executing);
}
}

return Promise.all(results);
}

// Process 100 items with max 5 concurrent requests
const results = await parallelWithLimit(items, 5, processItem);

Caching

Cache Responses (when appropriate):

class APICache {
constructor(ttl = 60000) { // 60 seconds default
this.cache = new Map();
this.ttl = ttl;
}

async get(key, fetchFn) {
const cached = this.cache.get(key);

if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}

const data = await fetchFn();
this.cache.set(key, { data, timestamp: Date.now() });
return data;
}

invalidate(key) {
this.cache.delete(key);
}

clear() {
this.cache.clear();
}
}

// Usage
const cache = new APICache(60000); // 1 minute TTL

const vaults = await cache.get('vaults', () => getVaults());
// Second call within 1 minute returns cached data
const vaultsCached = await cache.get('vaults', () => getVaults());

// Invalidate cache after mutation
await createVault(data);
cache.invalidate('vaults');

Cache Headers (respect server caching directives):

async function fetchWithCache(url) {
const response = await fetch(url);

// Check Cache-Control header
const cacheControl = response.headers.get('Cache-Control');
if (cacheControl?.includes('no-cache')) {
// Don't cache this response
}

// Check ETag for conditional requests
const etag = response.headers.get('ETag');
if (etag) {
// Store ETag for future requests
localStorage.setItem(`etag:${url}`, etag);
}

return response;
}

// Conditional request with If-None-Match
async function fetchIfModified(url) {
const etag = localStorage.getItem(`etag:${url}`);

const response = await fetch(url, {
headers: etag ? { 'If-None-Match': etag } : {}
});

if (response.status === 304) {
// Not modified, use cached data
return getCachedData(url);
}

return response;
}

Pagination

Always Use Pagination (for large datasets):

async function getAllVaults() {
const allVaults = [];
let page = 1;
let hasMore = true;

while (hasMore) {
const response = await fetch(
`https://api.torvussecurity.com/v1/vaults?page=${page}&limit=100`
);
const data = await response.json();

allVaults.push(...data.vaults);
hasMore = data.has_more;
page++;
}

return allVaults;
}

Cursor-Based Pagination (better for real-time data):

async function getAllVaultsWithCursor() {
const allVaults = [];
let cursor = null;

while (true) {
const url = cursor
? `https://api.torvussecurity.com/v1/vaults?cursor=${cursor}&limit=100`
: `https://api.torvussecurity.com/v1/vaults?limit=100`;

const response = await fetch(url);
const data = await response.json();

allVaults.push(...data.vaults);

if (!data.next_cursor) break;
cursor = data.next_cursor;
}

return allVaults;
}

Request Optimization

Use Compression

Enable Gzip Compression:

fetch('https://api.torvussecurity.com/v1/vaults', {
headers: {
'Accept-Encoding': 'gzip, deflate'
}
});

Request Only Needed Fields

Use Field Selection (if API supports it):

// Get only specific fields to reduce payload size
const response = await fetch(
'https://api.torvussecurity.com/v1/vaults?fields=id,name,created_at'
);

Minimize Payload Size

Send Only Changed Fields:

Bad (send entire object):

await updateVault(vaultId, {
name: 'New Name',
description: existingDescription,
recipients: existingRecipients,
// ... 20 more unchanged fields
});

Good (send only changed fields):

await updateVault(vaultId, {
name: 'New Name' // Only changed field
});

Idempotency

Use Idempotency Keys

Prevent Duplicate Requests:

const { v4: uuidv4 } = require('uuid');

async function createVaultIdempotent(vaultData) {
const idempotencyKey = uuidv4();

const response = await fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(vaultData)
});

return response.json();
}

Retry with Same Key (prevents duplicates):

async function createVaultWithRetry(vaultData) {
const idempotencyKey = uuidv4();

return await callAPIWithRetry(() =>
fetch('https://api.torvussecurity.com/v1/vaults', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey, // Same key for retries
// ... other headers
},
body: JSON.stringify(vaultData)
})
);
}

Monitoring & Logging

Log API Calls

Structured Logging:

const logger = require('winston');

async function callAPI(endpoint, options) {
const startTime = Date.now();
const requestId = uuidv4();

logger.info('API Request', {
requestId,
endpoint,
method: options.method || 'GET',
timestamp: new Date().toISOString()
});

try {
const response = await fetch(endpoint, options);
const duration = Date.now() - startTime;

logger.info('API Response', {
requestId,
endpoint,
status: response.status,
duration,
timestamp: new Date().toISOString()
});

return response;
} catch (error) {
const duration = Date.now() - startTime;

logger.error('API Error', {
requestId,
endpoint,
error: error.message,
duration,
timestamp: new Date().toISOString()
});

throw error;
}
}

Never Log Sensitive Data:

// Redact sensitive fields before logging
function sanitizeForLogging(data) {
const sanitized = { ...data };

// Redact sensitive fields
const sensitiveFields = ['api_key', 'password', 'token', 'secret'];
for (const field of sensitiveFields) {
if (sanitized[field]) {
sanitized[field] = '[REDACTED]';
}
}

return sanitized;
}

logger.info('Request data', sanitizeForLogging(requestData));

Track Metrics

Monitor API Performance:

class APIMetrics {
constructor() {
this.metrics = {
requests: 0,
errors: 0,
totalDuration: 0,
statusCodes: {}
};
}

recordRequest(status, duration) {
this.metrics.requests++;
this.metrics.totalDuration += duration;
this.metrics.statusCodes[status] = (this.metrics.statusCodes[status] || 0) + 1;

if (status >= 400) {
this.metrics.errors++;
}
}

getStats() {
return {
totalRequests: this.metrics.requests,
errorRate: (this.metrics.errors / this.metrics.requests * 100).toFixed(2) + '%',
avgDuration: (this.metrics.totalDuration / this.metrics.requests).toFixed(2) + 'ms',
statusCodes: this.metrics.statusCodes
};
}
}

const metrics = new APIMetrics();

// Record each API call
const startTime = Date.now();
const response = await fetch(/* ... */);
const duration = Date.now() - startTime;
metrics.recordRequest(response.status, duration);

// View stats
console.log(metrics.getStats());

Webhooks Best Practices

Verify Webhook Signatures

Always Verify (prevent spoofing):

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}

// Express endpoint
app.post('/webhooks/torvus', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-torvus-signature'];
const payload = req.body;

if (!verifyWebhookSignature(payload, signature, webhookSecret)) {
return res.status(401).send('Invalid signature');
}

// Process webhook
const event = JSON.parse(payload);
handleWebhookEvent(event);

res.status(200).send('OK');
});

Respond Quickly

Acknowledge Immediately, Process Asynchronously:

app.post('/webhooks/torvus', async (req, res) => {
// Verify signature
if (!verifyWebhookSignature(req.body, req.headers['x-torvus-signature'], secret)) {
return res.status(401).send('Invalid signature');
}

// Acknowledge immediately (within 5 seconds)
res.status(200).send('OK');

// Process asynchronously (using queue)
await queue.add('process-webhook', {
event: JSON.parse(req.body)
});
});

// Queue worker (processes webhooks in background)
queue.process('process-webhook', async (job) => {
const { event } = job.data;
await handleWebhookEvent(event);
});

Handle Duplicates

Idempotent Processing:

const processedEvents = new Set();

async function handleWebhookEvent(event) {
// Check if already processed
if (processedEvents.has(event.id)) {
console.log(`Event ${event.id} already processed, skipping`);
return;
}

// Process event
await processEvent(event);

// Mark as processed
processedEvents.add(event.id);

// Cleanup old events (prevent memory leak)
if (processedEvents.size > 10000) {
const oldestEvents = Array.from(processedEvents).slice(0, 5000);
oldestEvents.forEach(id => processedEvents.delete(id));
}
}

Testing

Use Test Mode

Separate Test and Production:

const config = {
development: {
apiKey: process.env.TORVUS_TEST_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
},
production: {
apiKey: process.env.TORVUS_LIVE_API_KEY,
baseUrl: 'https://api.torvussecurity.com/v1'
}
};

const env = process.env.NODE_ENV || 'development';
const { apiKey, baseUrl } = config[env];

Mock API Responses

Use Mocking for Tests:

// Jest test
jest.mock('node-fetch');
const fetch = require('node-fetch');

test('createVault creates vault successfully', async () => {
fetch.mockResolvedValueOnce({
ok: true,
status: 201,
json: async () => ({
id: 'vault_123',
name: 'Test Vault'
})
});

const vault = await createVault({ name: 'Test Vault' });

expect(vault.id).toBe('vault_123');
expect(fetch).toHaveBeenCalledWith(
expect.stringContaining('/vaults'),
expect.objectContaining({
method: 'POST'
})
);
});

Integration Tests

Test Against Sandbox:

// integration.test.js
describe('Torvus API Integration', () => {
let testVaultId;

beforeAll(async () => {
// Use test API key
process.env.TORVUS_API_KEY = process.env.TORVUS_TEST_API_KEY;
});

test('create vault', async () => {
const vault = await createVault({
name: 'Integration Test Vault'
});

expect(vault.id).toMatch(/^vault_/);
testVaultId = vault.id;
});

test('update vault', async () => {
const updated = await updateVault(testVaultId, {
name: 'Updated Name'
});

expect(updated.name).toBe('Updated Name');
});

afterAll(async () => {
// Cleanup: delete test vault
await deleteVault(testVaultId);
});
});

SDK Usage

Use Official SDKs

JavaScript/TypeScript SDK:

const { TorvusClient } = require('@torvus/sdk');

const torvus = new TorvusClient({
apiKey: process.env.TORVUS_API_KEY
});

// SDKs handle retries, rate limiting, and errors automatically
const vaults = await torvus.vaults.list();
const vault = await torvus.vaults.create({ name: 'My Vault' });

Python SDK:

from torvus import TorvusClient

torvus = TorvusClient(api_key=os.environ['TORVUS_API_KEY'])

vaults = torvus.vaults.list()
vault = torvus.vaults.create(name='My Vault')

Benefits of Official SDKs:

  • Automatic retries with exponential backoff
  • Built-in rate limiting
  • Type safety (TypeScript)
  • Better error messages
  • Pagination helpers
  • Webhook signature verification
  • Regular updates and security patches

Common Pitfalls

Don't Poll Unnecessarily

Bad:

// Polling every second (wasteful)
setInterval(async () => {
const vaults = await getVaults();
updateUI(vaults);
}, 1000);

Good:

// Use webhooks for real-time updates
app.post('/webhooks/vault-updated', (req, res) => {
const event = req.body;
updateUI(event.data);
res.status(200).send('OK');
});

// Or poll less frequently
setInterval(async () => {
const vaults = await getVaults();
updateUI(vaults);
}, 60000); // Every 60 seconds

Don't Ignore Pagination

Bad:

// Only gets first page (max 100 results)
const vaults = await getVaults();

Good:

// Gets all results across all pages
const vaults = await getAllVaults(); // Implements pagination loop

Don't Hardcode IDs

Bad:

const vaultId = 'vault_abc123'; // Hardcoded, breaks across environments

Good:

const vaultId = process.env.TORVUS_VAULT_ID; // Environment-specific
// Or dynamically fetch: const vault = await findVaultByName('My Vault');

Checklist

Before Going to Production:

  • API keys stored in environment variables (never hardcoded)
  • Different API keys for dev, staging, and production
  • Error handling for all API calls (including network errors)
  • Retry logic with exponential backoff
  • Rate limiting handled (check headers, implement queuing)
  • Pagination implemented for list endpoints
  • Requests use HTTPS (never HTTP)
  • Sensitive data never logged
  • Webhook signatures verified
  • Idempotency keys used for mutations
  • Timeouts configured (don't wait forever)
  • Monitoring and logging in place
  • Integration tests passing
  • SDK used (if available for your language)
  • API key rotation plan in place
  • Documented API usage for your team


Last Updated: October 8, 2025