Best Practices
Follow these patterns to build robust, efficient integrations with the Vantage API. Learn how to handle pagination, caching, errors, and more.
Pagination
All list endpoints return paginated results. Never assume you’ve received all data in a single request.
Basic Pagination
async function getAllClients() {
const clients = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`https://api.govantage.co/v1/clients?page=${page}&per_page=100`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
);
const data = await response.json();
clients.push(...data.data);
hasMore = page < data.pagination.total_pages;
page++;
}
return clients;
}Pagination Response
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 156,
"total_pages": 8
}
}Use per_page=100 (the maximum) to minimize API calls when fetching large datasets.
Rate Limit Handling
Vantage enforces rate limits to ensure fair usage. Handle 429 responses gracefully.
Retry with Backoff
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After')) || 30;
console.log(`Rate limited. Retrying in ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue;
}
if (response.status >= 500) {
// Server error - exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}Monitor Rate Limit Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1706191800function checkRateLimit(response) {
const remaining = parseInt(response.headers.get('X-RateLimit-Remaining'));
const reset = parseInt(response.headers.get('X-RateLimit-Reset'));
if (remaining < 10) {
const waitTime = (reset * 1000) - Date.now();
console.warn(`Low rate limit: ${remaining} remaining. Resets in ${waitTime}ms`);
}
}Caching
Cache responses when appropriate to reduce API calls and improve performance.
What to Cache
| Resource | Cache Duration | Notes |
|---|---|---|
| Users list | 5 minutes | Changes infrequently |
| Clients list | 1-5 minutes | Moderate changes |
| Tags | 10 minutes | Rarely changes |
| Time entries | Don’t cache | Changes frequently |
| Invoices | 1 minute | Real-time accuracy needed |
Cache Invalidation
Invalidate cache when:
- You make a write operation (POST, PUT, DELETE)
- You receive a webhook for that resource type
- TTL expires
const cache = new Map();
async function getCachedClients() {
const cacheKey = 'clients';
const cached = cache.get(cacheKey);
if (cached && Date.now() < cached.expiresAt) {
return cached.data;
}
const response = await fetch('https://api.govantage.co/v1/clients', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
cache.set(cacheKey, {
data: data.data,
expiresAt: Date.now() + (5 * 60 * 1000) // 5 minutes
});
return data.data;
}
// Invalidate on webhook
function handleWebhook(event) {
if (event.type.startsWith('client.')) {
cache.delete('clients');
}
}Error Handling
Build resilient integrations by handling errors gracefully.
Error Response Structure
{
"error": {
"code": "invalid_request",
"message": "Email is required",
"field": "email",
"doc_url": "https://docs.govantage.co/api-reference/errors"
}
}Comprehensive Error Handler
class VantageAPIError extends Error {
constructor(response, body) {
super(body.error?.message || 'API Error');
this.status = response.status;
this.code = body.error?.code;
this.field = body.error?.field;
}
}
async function apiRequest(endpoint, options = {}) {
const response = await fetch(`https://api.govantage.co/v1${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
...options.headers
}
});
const body = await response.json();
if (!response.ok) {
throw new VantageAPIError(response, body);
}
return body;
}
// Usage
try {
const client = await apiRequest('/clients', {
method: 'POST',
body: JSON.stringify({ name: 'Acme Corp' })
});
} catch (error) {
if (error instanceof VantageAPIError) {
switch (error.code) {
case 'duplicate':
console.log('Client already exists');
break;
case 'invalid_request':
console.log(`Invalid field: ${error.field}`);
break;
case 'rate_limit_exceeded':
// Retry logic
break;
default:
console.error('API error:', error.message);
}
} else {
// Network or other error
console.error('Request failed:', error);
}
}Idempotency
Make requests safely repeatable to handle network failures.
Use Idempotency Keys
For POST requests that create resources, include an idempotency key:
curl -X POST "https://api.govantage.co/v1/invoices" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Idempotency-Key: inv-acme-jan-2026" \
-H "Content-Type: application/json" \
-d '{"client_id": "cli_xxxxx", ...}'If the request is retried with the same key, Vantage returns the original response instead of creating a duplicate.
async function createInvoice(data, idempotencyKey) {
return apiRequest('/invoices', {
method: 'POST',
headers: {
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(data)
});
}
// Generate deterministic key from data
const key = `inv-${data.client_id}-${data.invoice_date}`;
const invoice = await createInvoice(data, key);Idempotency keys expire after 24 hours. Use meaningful keys based on your data to prevent accidental duplicates.
Batch Operations
Minimize API calls by using bulk endpoints when available.
Bulk Create
# Instead of 10 separate calls:
curl -X POST "https://api.govantage.co/v1/time-entries/bulk" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"entries": [
{"user_id": "usr_xxx", "date": "2026-01-25", "hours": 2, ...},
{"user_id": "usr_xxx", "date": "2026-01-25", "hours": 3, ...},
...
]
}'Bulk Update
curl -X PUT "https://api.govantage.co/v1/tickets/bulk" \
-H "Authorization: Bearer vnt_sk_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"ids": ["tkt_xxx", "tkt_yyy", "tkt_zzz"],
"updates": {"status_id": "stat_closed"}
}'Webhooks vs Polling
Prefer webhooks over polling for real-time updates.
Don’t: Poll for Changes
// Bad: Inefficient and wastes API quota
setInterval(async () => {
const invoices = await getInvoices({ status: 'paid' });
// Check for new payments
}, 60000);Do: Use Webhooks
// Good: Instant notifications, no wasted calls
app.post('/webhooks/vantage', (req, res) => {
const event = verifyWebhook(req);
if (event.type === 'invoice.paid') {
handlePayment(event.data);
}
res.status(200).send('OK');
});Request Optimization
Use Field Selection
Request only the fields you need using the fields parameter:
# Only get id, name, and status
curl "https://api.govantage.co/v1/clients?fields=id,name,status" \
-H "Authorization: Bearer vnt_sk_live_xxxxx"Use Includes Wisely
The include parameter fetches related data in one call:
# Get project with tickets and time in one request
curl "https://api.govantage.co/v1/projects/prj_xxx?include=tickets,time_entries" \
-H "Authorization: Bearer vnt_sk_live_xxxxx"But don’t over-include—only request what you need.
Filter Server-Side
Let the API filter instead of fetching everything:
// Bad: Fetch all, filter client-side
const allTickets = await getTickets();
const openTickets = allTickets.filter(t => t.status === 'open');
// Good: Filter server-side
const openTickets = await getTickets({ status: 'open' });Security
Store Keys Securely
// Bad: Hardcoded
const apiKey = 'vnt_sk_live_xxxxx';
// Good: Environment variable
const apiKey = process.env.VANTAGE_API_KEY;Use Minimal Permissions
Create API keys with only the permissions needed:
- Read-only key for analytics dashboards
- Scoped key for specific integrations
- Full-access key only for admin tools
Validate Webhook Signatures
Always verify webhook signatures before processing:
app.post('/webhooks', (req, res) => {
try {
verifySignature(req.body, req.headers['x-vantage-signature']);
} catch (e) {
return res.status(401).send('Invalid signature');
}
// Process webhook...
});Logging
Log API interactions for debugging and auditing.
What to Log
async function apiRequest(endpoint, options) {
const startTime = Date.now();
const requestId = generateRequestId();
console.log({
type: 'api_request',
requestId,
endpoint,
method: options.method || 'GET'
});
try {
const response = await fetch(/* ... */);
console.log({
type: 'api_response',
requestId,
status: response.status,
duration: Date.now() - startTime,
rateLimit: response.headers.get('X-RateLimit-Remaining')
});
return response;
} catch (error) {
console.error({
type: 'api_error',
requestId,
error: error.message,
duration: Date.now() - startTime
});
throw error;
}
}Don’t Log Sensitive Data
- Never log API keys
- Mask or omit PII (emails, names)
- Don’t log full request bodies in production
Testing
Use Test Mode
Test API keys (vnt_sk_test_) work identically to live keys but operate in isolation:
- Create test data freely
- No real invoices sent
- No rate limit impact on production
Mock for Unit Tests
// Mock the API for unit tests
jest.mock('./vantageApi', () => ({
getClients: jest.fn().mockResolvedValue([
{ id: 'cli_test', name: 'Test Client' }
])
}));Checklist
Before going to production:
- Handle rate limits with exponential backoff
- Implement pagination for all list endpoints
- Verify webhook signatures
- Store API keys in environment variables
- Set up proper error handling and logging
- Use idempotency keys for create operations
- Test with test mode API keys
- Cache frequently-accessed, rarely-changed data
Next Steps
- Rate Limits - Understand limits
- Error Handling - Error codes
- Webhooks - Real-time events