Best Practices
Follow these recommendations for secure, reliable, and performant integrations with Vobiz. Learn from our experience and avoid common pitfalls.
Security Best Practices
Never Hardcode Credentials
Bad Practice
const API_KEY = "sk_live_abc123def456"; // NEVER DO THIS
const PASSWORD = "my_secret_password"; // NEVER DO THISGood Practice
// Load from environment variables
const API_KEY = process.env.VOBIZ_API_KEY;
const PASSWORD = process.env.VOBIZ_PASSWORD;
// Or use a secrets manager
const secrets = await secretsManager.getSecret("vobiz/credentials");Use HTTPS for All Requests
Always use https:// URLs when calling the API. Never send authentication tokens or sensitive data over unencrypted HTTP connections. Our API only responds to HTTPS.
Rotate Credentials Regularly
- Change trunk passwords every 90 days
- Change account passwords regularly and invalidate old refresh tokens
- Immediately rotate if credentials are suspected to be compromised
- Use sub-accounts for different applications/teams to limit blast radius
Whitelist IP Addresses
For production SIP trunks with static IPs, use IP authentication instead of passwords:
{
"authType": "ip",
"ipWhitelist": [
"203.0.113.10", // Production PBX
"203.0.113.11" // Backup PBX
]
}This eliminates password-based attacks and simplifies configuration for trusted systems.
Validate Input on Client Side
Implement client-side validation before sending requests to prevent injection attacks:
- Sanitize phone numbers (remove special characters, validate E.164 format)
- Validate email addresses match RFC 5322 spec
- Limit string lengths (trunk names, usernames, etc.)
- Reject suspicious patterns (SQL injection attempts, XSS payloads)
Authentication & Authorization
Token Storage
Frontend Applications
- Store access tokens in memory or sessionStorage (expires when tab closes)
- Never store tokens in localStorage (vulnerable to XSS attacks)
- Use httpOnly cookies for refresh tokens if possible
Backend Applications
- Store tokens in environment variables or secrets manager
- Encrypt tokens at rest if storing in database
- Use secure memory storage for short-lived tokens
Automatic Token Refresh
Implement automatic token refresh before expiration (30 minutes for access tokens):
async function makeAuthenticatedRequest(url, options) {
// Check if token expires in next 5 minutes
if (tokenExpiresAt - Date.now() < 5 * 60 * 1000) {
await refreshAccessToken();
}
return fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${accessToken}`
}
});
}
async function refreshAccessToken() {
const response = await fetch('/api/v1/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken })
});
const data = await response.json();
accessToken = data.access_token;
tokenExpiresAt = Date.now() + (30 * 60 * 1000);
}Use Sub-Accounts for Separation
Create separate sub-accounts for different applications, environments, or teams:
- Development: Separate sub-account for testing
- Production: Dedicated sub-account with stricter controls
- Partners: Sub-accounts for each integration partner
- Departments: Sales, Support, Engineering teams get own sub-accounts
Performance Optimization
Implement Caching
Cache infrequently changing data to reduce API calls:
| Resource | Cache TTL | Notes |
|---|---|---|
| Account info | 15 minutes | Rarely changes |
| Trunk list | 5 minutes | Invalidate on create/delete |
| Balance | 1 minute | Frequently updated |
| CDRs | No cache | Always fetch fresh |
Use Pagination Wisely
Don't fetch all records at once. Use pagination for large datasets:
// Fetch CDRs in batches of 100
async function getAllCDRs(accountId, startDate, endDate) {
const allCDRs = [];
let offset = 0;
const limit = 100;
while (true) {
const response = await fetch(
`/api/v1/account/${accountId}/cdr?start_date=${startDate}&end_date=${endDate}&limit=${limit}&offset=${offset}`
);
const data = await response.json();
allCDRs.push(...data.data.cdrs);
if (!data.data.pagination.hasMore) break;
offset += limit;
}
return allCDRs;
}Batch Operations
Instead of making multiple individual requests, consider implementing client-side batching:
- Batch CDR queries by date ranges instead of querying each day separately
- Combine multiple resource updates in a single transaction when possible
- Use bulk operations for importing/exporting large datasets
Connection Pooling
Reuse HTTP connections instead of creating new ones for every request:
const https = require('https');
// Create agent with connection pooling
const agent = new https.Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
// Reuse agent for all requests
fetch(url, { agent });Reliability & Error Handling
Implement Retry Logic
Retry failed requests with exponential backoff for transient errors:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry 4xx errors (client errors)
if (response.status >= 400 && response.status < 500) {
return response;
}
// Retry 5xx errors and network failures
if (response.ok) {
return response;
}
if (attempt === maxRetries) {
throw new Error(`Failed after ${maxRetries} retries`);
}
// Exponential backoff: 1s, 2s, 4s, 8s...
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}Circuit Breaker Pattern
Prevent cascading failures by implementing a circuit breaker:
- Track consecutive failures for each service
- After 5 failures, "open" circuit and fail fast for 30 seconds
- After timeout, allow one request through ("half-open" state)
- If successful, close circuit; if failed, reopen for longer
Handle Rate Limits Gracefully
When you receive a 429 (Rate Limit) response:
async function handleRateLimit(response) {
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited. Retrying after ${retryAfter} seconds`);
await new Promise(resolve =>
setTimeout(resolve, retryAfter * 1000)
);
// Retry the request
return fetch(url, options);
}
return response;
}Idempotency Keys
For critical operations (purchases, transactions), use idempotency keys to prevent duplicates:
const idempotencyKey = `purchase-${Date.now()}-${Math.random()}`;
await fetch('/api/v1/account/ACC123/numbers/purchase', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ phoneNumber: '+14155551234' })
});Monitoring & Logging
Log All API Requests
Maintain detailed logs for debugging and auditing:
function logAPIRequest(method, url, status, duration, requestId) {
console.log({
timestamp: new Date().toISOString(),
method,
url,
status,
duration: `${duration}ms`,
requestId,
service: 'vobiz-api'
});
}Track Key Metrics
- • API request success/failure rates
- • Average response times per endpoint
- • Error distribution by error code
- • Token refresh frequency
- • Balance consumption rate
- • Call success rates (ASR)
- • Rate limit violations
Set Up Alerts
Configure alerts for critical events:
- Balance Low: Alert when balance drops below threshold
- Error Spike: Alert when error rate exceeds 5% of requests
- Trunk Down: Alert when trunk authentication fails repeatedly
- Rate Limit Hit: Alert when approaching rate limits
Testing Strategies
Use Separate Accounts for Testing
Create dedicated test accounts/sub-accounts with separate credentials. Never test in production with real customer data or active trunks.
Test Error Scenarios
Don't just test happy paths. Verify your application handles errors correctly: invalid credentials, insufficient balance, rate limits, network timeouts, etc.
Automate Integration Tests
Use the Postman collection with Newman CLI in your CI/CD pipeline to catch integration regressions early. Run tests on every commit to main branch.
Monitor Test Call Quality
Make regular test calls and verify: • Call connects within 3 seconds (low PDD)
• Audio quality is clear (no packet loss)
• CDR is generated correctly
• Balance is debited accurately
Load Testing
Before going live, test with realistic load: • Simulate expected CPS (calls per second)
• Test concurrent call limits
• Verify rate limiting behaves as expected
• Monitor API response times under load