Skip to main content
Skip to Content

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: 1706191800
function 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

ResourceCache DurationNotes
Users list5 minutesChanges infrequently
Clients list1-5 minutesModerate changes
Tags10 minutesRarely changes
Time entriesDon’t cacheChanges frequently
Invoices1 minuteReal-time accuracy needed

Cache Invalidation

Invalidate cache when:

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:

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


Testing

Use Test Mode

Test API keys (vnt_sk_test_) work identically to live keys but operate in isolation:

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:


Next Steps