Overview
This guide covers best practices for building robust, efficient, and secure integrations with the GetBill API. Following these guidelines will help ensure your application performs well and provides a great user experience.Authentication & Security
Secure Token Storage
Store access tokens securely and never expose them in client-side code or logs.
Token Refresh
Implement automatic token refresh to handle expiration gracefully.
Minimal Scopes
Request only the minimum scopes required for your application’s functionality.
HTTPS Only
Always use HTTPS for all API communications in production.
Token Management Example
Copy
class TokenManager {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.refreshToken = null;
this.expiresAt = null;
}
async getValidToken() {
// Check if current token is still valid
if (this.accessToken && this.expiresAt > Date.now() + 60000) {
return this.accessToken;
}
// Try to refresh if we have a refresh token
if (this.refreshToken) {
try {
await this.refreshAccessToken();
return this.accessToken;
} catch (error) {
console.log('Token refresh failed, getting new token');
}
}
// Get a new token
await this.getNewToken();
return this.accessToken;
}
async refreshAccessToken() {
const response = await fetch('https://getbill.io/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.clientId,
client_secret: this.clientSecret
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokenData = await response.json();
this.updateTokens(tokenData);
}
updateTokens(tokenData) {
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token || this.refreshToken;
this.expiresAt = Date.now() + (tokenData.expires_in * 1000) - 60000; // 1 min buffer
}
}
Error Handling
Implement Comprehensive Error Handling
Copy
class APIClient {
async makeRequest(endpoint, options = {}) {
const maxRetries = 3;
let retries = 0;
while (retries < maxRetries) {
try {
const response = await fetch(`https://getbill.io/external-api/v1${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${await this.getValidToken()}`,
'Content-Type': 'application/json',
...options.headers
}
});
// Handle different response codes
if (response.ok) {
return await response.json();
}
const error = await response.json();
switch (response.status) {
case 401:
// Token expired, refresh and retry
await this.refreshToken();
retries++;
continue;
case 429:
// Rate limited, wait and retry
const retryAfter = response.headers.get('Retry-After') || Math.pow(2, retries);
await this.sleep(retryAfter * 1000);
retries++;
continue;
case 500:
case 502:
case 503:
// Server error, retry with backoff
if (retries < maxRetries - 1) {
await this.sleep(Math.pow(2, retries) * 1000);
retries++;
continue;
}
break;
default:
// Client error, don't retry
throw new APIError(error.message, response.status, error.details);
}
throw new APIError(error.message, response.status, error.details);
} catch (error) {
if (retries === maxRetries - 1) {
throw error;
}
// Network error, retry with backoff
await this.sleep(Math.pow(2, retries) * 1000);
retries++;
}
}
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class APIError extends Error {
constructor(message, status, details = null) {
super(message);
this.name = 'APIError';
this.status = status;
this.details = details;
}
}
Rate Limiting & Performance
Respect Rate Limits
Copy
class RateLimitedClient {
constructor() {
this.requestQueue = [];
this.processing = false;
this.rateLimitRemaining = null;
this.rateLimitReset = null;
}
async enqueueRequest(requestFn) {
return new Promise((resolve, reject) => {
this.requestQueue.push({ requestFn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.requestQueue.length === 0) return;
this.processing = true;
while (this.requestQueue.length > 0) {
// Check rate limit
if (this.rateLimitRemaining !== null && this.rateLimitRemaining < 10) {
const waitTime = this.getWaitTime();
if (waitTime > 0) {
await this.sleep(waitTime);
}
}
const { requestFn, resolve, reject } = this.requestQueue.shift();
try {
const response = await requestFn();
this.updateRateLimitInfo(response);
resolve(response);
} catch (error) {
reject(error);
}
// Small delay between requests
await this.sleep(100);
}
this.processing = false;
}
updateRateLimitInfo(response) {
this.rateLimitRemaining = parseInt(response.headers.get('X-RateLimit-Remaining')) || null;
this.rateLimitReset = parseInt(response.headers.get('X-RateLimit-Reset')) || null;
}
getWaitTime() {
if (!this.rateLimitReset) return 0;
const now = Math.floor(Date.now() / 1000);
return Math.max(0, (this.rateLimitReset - now) * 1000);
}
}
Optimize Pagination
Copy
// ✅ Good: Efficient pagination
async function getAllDebts(client) {
const allDebts = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await client.getDebts({
page,
limit: 100 // Use larger page sizes
});
allDebts.push(...response.data);
hasMore = page < response.pagination.pages;
page++;
// Add small delay to be nice to the API
if (hasMore) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return allDebts;
}
// ❌ Bad: Many small requests
async function getAllDebtsInefficient(client) {
const allDebts = [];
for (let page = 1; page <= 50; page++) {
const response = await client.getDebts({ page, limit: 10 });
allDebts.push(...response.data);
}
return allDebts;
}
Data Management
Caching Strategy
Copy
class CachedAPIClient {
constructor(apiClient, cacheTimeout = 300000) { // 5 minutes
this.apiClient = apiClient;
this.cache = new Map();
this.cacheTimeout = cacheTimeout;
}
async getCachedData(key, fetchFn) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
return cached.data;
}
const data = await fetchFn();
this.cache.set(key, {
data,
timestamp: Date.now()
});
return data;
}
async getCompanyProfile() {
return this.getCachedData('company:profile', () =>
this.apiClient.makeRequest('/company/profile')
);
}
async getDebt(id) {
return this.getCachedData(`debt:${id}`, () =>
this.apiClient.makeRequest(`/debts/${id}`)
);
}
invalidateCache(pattern) {
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
// Invalidate cache when data is modified
async updateDebt(id, data) {
const result = await this.apiClient.makeRequest(`/debts/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
});
this.invalidateCache(`debt:${id}`);
this.invalidateCache('debts:list');
return result;
}
}
Validate Data Before Sending
Copy
class DebtValidator {
static validate(debtData) {
const errors = {};
// Required fields
if (!debtData.firstname?.trim()) {
errors.firstname = 'First name is required';
}
if (!debtData.lastname?.trim()) {
errors.lastname = 'Last name is required';
}
if (!debtData.phone?.trim()) {
errors.phone = 'Phone number is required';
}
if (!debtData.amount || debtData.amount <= 0) {
errors.amount = 'Amount must be greater than 0';
}
if (!debtData.currency?.trim()) {
errors.currency = 'Currency is required';
}
// Format validation
if (debtData.email && !this.isValidEmail(debtData.email)) {
errors.email = 'Invalid email format';
}
if (debtData.birthdate && !this.isValidDate(debtData.birthdate)) {
errors.birthdate = 'Invalid date format (use YYYY-MM-DD)';
}
return {
isValid: Object.keys(errors).length === 0,
errors
};
}
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
static isValidDate(dateString) {
return /^\d{4}-\d{2}-\d{2}$/.test(dateString) && !isNaN(Date.parse(dateString));
}
}
// Usage
async function createDebt(client, debtData) {
const validation = DebtValidator.validate(debtData);
if (!validation.isValid) {
throw new ValidationError('Invalid debt data', validation.errors);
}
return client.makeRequest('/debts', {
method: 'POST',
body: JSON.stringify(debtData)
});
}
Logging & Monitoring
Implement Comprehensive Logging
Copy
class APILogger {
constructor(logLevel = 'info') {
this.logLevel = logLevel;
this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
}
log(level, message, context = {}) {
if (this.levels[level] <= this.levels[this.logLevel]) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
level,
message,
context
}));
}
}
logAPICall(endpoint, method, duration, status, error = null) {
const context = {
endpoint,
method,
duration,
status,
success: status >= 200 && status < 300
};
if (error) {
context.error = error.message;
this.log('error', `API call failed: ${method} ${endpoint}`, context);
} else {
this.log('info', `API call: ${method} ${endpoint}`, context);
}
}
logRateLimit(remaining, reset) {
this.log('info', 'Rate limit status', { remaining, reset });
if (remaining < 50) {
this.log('warn', 'Rate limit running low', { remaining, reset });
}
}
}
// Usage with API client
class MonitoredAPIClient extends APIClient {
constructor() {
super();
this.logger = new APILogger();
}
async makeRequest(endpoint, options = {}) {
const startTime = Date.now();
const method = options.method || 'GET';
try {
const response = await super.makeRequest(endpoint, options);
const duration = Date.now() - startTime;
this.logger.logAPICall(endpoint, method, duration, response.status || 200);
// Log rate limit info
if (response.headers) {
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (remaining !== null) {
this.logger.logRateLimit(parseInt(remaining), parseInt(reset));
}
}
return response;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.logAPICall(endpoint, method, duration, error.status || 0, error);
throw error;
}
}
}
Testing Your Integration
Unit Testing Example
Copy
// Mock API client for testing
class MockAPIClient {
constructor() {
this.responses = new Map();
}
mockResponse(endpoint, response) {
this.responses.set(endpoint, response);
}
async makeRequest(endpoint) {
const response = this.responses.get(endpoint);
if (!response) {
throw new Error(`No mock response for ${endpoint}`);
}
return response;
}
}
// Test example
describe('Debt Service', () => {
let debtService;
let mockClient;
beforeEach(() => {
mockClient = new MockAPIClient();
debtService = new DebtService(mockClient);
});
it('should create a debt successfully', async () => {
const debtData = {
firstname: 'John',
lastname: 'Doe',
phone: '+33123456789',
amount: 1000,
currency: 'EUR'
};
mockClient.mockResponse('/debts', {
error: false,
data: { id: 'abc123', ...debtData }
});
const result = await debtService.createDebt(debtData);
expect(result.data.id).toBe('abc123');
});
it('should handle validation errors', async () => {
const invalidData = {
firstname: '',
amount: -100
};
await expect(debtService.createDebt(invalidData))
.rejects
.toThrow('Invalid debt data');
});
});
Performance Optimization
Batch Operations
Group multiple operations together when possible to reduce API calls.
Parallel Requests
Make independent requests in parallel rather than sequentially.
Efficient Filtering
Use API filters to reduce data transfer and processing time.
Pagination Optimization
Use appropriate page sizes and implement efficient pagination logic.
Parallel Processing Example
Copy
// ✅ Good: Parallel requests
async function getDebtDetails(client, debtIds) {
const promises = debtIds.map(id => client.getDebt(id));
return Promise.all(promises);
}
// ✅ Good: Parallel with error handling
async function getDebtDetailsWithErrorHandling(client, debtIds) {
const promises = debtIds.map(async (id) => {
try {
return await client.getDebt(id);
} catch (error) {
console.error(`Failed to get debt ${id}:`, error.message);
return null;
}
});
const results = await Promise.all(promises);
return results.filter(debt => debt !== null);
}
// ❌ Bad: Sequential requests
async function getDebtDetailsSequential(client, debtIds) {
const debts = [];
for (const id of debtIds) {
const debt = await client.getDebt(id);
debts.push(debt);
}
return debts;
}
Security Considerations
Input Sanitization
Copy
function sanitizeInput(input) {
if (typeof input !== 'string') return input;
return input
.trim()
.replace(/[<>]/g, '') // Remove potential HTML tags
.substring(0, 1000); // Limit length
}
function sanitizeDebtData(data) {
return {
...data,
firstname: sanitizeInput(data.firstname),
lastname: sanitizeInput(data.lastname),
email: sanitizeInput(data.email),
address: sanitizeInput(data.address),
object: sanitizeInput(data.object)
};
}
Environment Configuration
Copy
// config.js
const config = {
development: {
apiUrl: 'https://api-dev.getbill.io',
clientId: process.env.GETBILL_CLIENT_ID_DEV,
clientSecret: process.env.GETBILL_CLIENT_SECRET_DEV,
logLevel: 'debug'
},
production: {
apiUrl: 'https://getbill.io',
clientId: process.env.GETBILL_CLIENT_ID,
clientSecret: process.env.GETBILL_CLIENT_SECRET,
logLevel: 'info'
}
};
const environment = process.env.NODE_ENV || 'development';
module.exports = config[environment];