Appearance
Authentication & Security
Table of Contents
- Overview
- Authentication Methods
- Token Acquisition
- Token Management
- Security Best Practices
- Authentication Examples
- Error Scenarios
Overview
The Bill Tracking API uses a header-based authentication system with multi-tenant isolation. Every request must include proper authentication headers to ensure secure access to your organization's data.
Key Security Features
- Multi-tenant Architecture: Complete data isolation between organizations
- HTTPS Required: All API communications must use TLS/SSL encryption
- Rate Limiting: Protection against abuse and DOS attacks
- Audit Logging: Complete request tracking for compliance
- Version Control: API versioning for backward compatibility
Authentication Methods
Primary Authentication: Header-Based Tenant Authentication
The API requires two mandatory headers for all requests:
| Header | Type | Description | Example |
|---|---|---|---|
api-version | String | API version identifier | 1.0 |
tenant-code | String | Unique organization identifier | TENANT-ABC123 |
Request Format
http
GET /api/1.0/endpoint HTTP/1.1
Host: engagifii-billtracking.azurewebsites.net
api-version: 1.0
tenant-code: YOUR_TENANT_CODE
Content-Type: application/jsonAuthentication Flow
mermaid
sequenceDiagram
participant Client
participant API
participant TenantService
participant Database
Client->>API: Request with headers
API->>API: Validate headers present
API->>TenantService: Verify tenant-code
TenantService->>Database: Check tenant status
Database-->>TenantService: Tenant details
TenantService-->>API: Authentication result
API-->>Client: Response or 401/403 errorToken Acquisition
Step 1: Obtain Your Tenant Code
Contact the Engagifii onboarding team to receive:
- Your unique
tenant-code - API access permissions
- Rate limit configuration
- Allowed IP ranges (if applicable)
Step 2: Store Credentials Securely
javascript
// Node.js - Using environment variables
require('dotenv').config();
const config = {
tenantCode: process.env.BILLTRACKING_TENANT_CODE,
apiVersion: process.env.BILLTRACKING_API_VERSION || '1.0',
baseUrl: process.env.BILLTRACKING_API_URL || 'https://engagifii-billtracking.azurewebsites.net'
};
// Never hardcode credentials in source codeStep 3: Validate Your Access
bash
# Test your credentials
curl -X GET "https://engagifii-billtracking.azurewebsites.net/api/1.0/dropdown/states" \
-H "api-version: 1.0" \
-H "tenant-code: $TENANT_CODE" \
-H "Content-Type: application/json" \
-w "\nHTTP Status: %{http_code}\n"Token Management
Configuration Management
Environment-Based Configuration
python
# Python - config.py
import os
from typing import Optional
class BillTrackingConfig:
def __init__(self):
self.tenant_code = self._get_required_env('BILLTRACKING_TENANT_CODE')
self.api_version = os.getenv('BILLTRACKING_API_VERSION', '1.0')
self.base_url = os.getenv('BILLTRACKING_API_URL',
'https://engagifii-billtracking.azurewebsites.net')
self.timeout = int(os.getenv('BILLTRACKING_TIMEOUT', '30'))
self.retry_count = int(os.getenv('BILLTRACKING_RETRY_COUNT', '3'))
@staticmethod
def _get_required_env(key: str) -> str:
value = os.getenv(key)
if not value:
raise ValueError(f"Required environment variable {key} is not set")
return value
# Usage
config = BillTrackingConfig()Secure Storage Options
javascript
// AWS Secrets Manager Example
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();
async function getApiCredentials() {
try {
const secret = await secretsManager.getSecretValue({
SecretId: 'billtracking-api-credentials'
}).promise();
return JSON.parse(secret.SecretString);
} catch (error) {
console.error('Failed to retrieve credentials:', error);
throw error;
}
}Header Injection Patterns
Axios Interceptor (JavaScript)
javascript
const axios = require('axios');
// Create configured instance
const apiClient = axios.create({
baseURL: 'https://engagifii-billtracking.azurewebsites.net',
timeout: 30000
});
// Add authentication headers to all requests
apiClient.interceptors.request.use(
config => {
config.headers['api-version'] = process.env.API_VERSION || '1.0';
config.headers['tenant-code'] = process.env.TENANT_CODE;
config.headers['Content-Type'] = 'application/json';
// Optional: Add request ID for tracking
config.headers['X-Request-ID'] = generateRequestId();
return config;
},
error => {
return Promise.reject(error);
}
);
function generateRequestId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}HTTP Client Wrapper (Python)
python
import requests
from typing import Dict, Any, Optional
import logging
class BillTrackingAPIClient:
def __init__(self, tenant_code: str, api_version: str = '1.0'):
self.base_url = 'https://engagifii-billtracking.azurewebsites.net'
self.tenant_code = tenant_code
self.api_version = api_version
self.session = requests.Session()
self._setup_session()
def _setup_session(self):
"""Configure session with default headers"""
self.session.headers.update({
'api-version': self.api_version,
'tenant-code': self.tenant_code,
'Content-Type': 'application/json',
'User-Agent': 'BillTracking-Python-Client/1.0'
})
def get(self, endpoint: str, params: Optional[Dict] = None) -> Dict[str, Any]:
"""Execute GET request with authentication"""
url = f"{self.base_url}/api/{self.api_version}/{endpoint}"
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def post(self, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
"""Execute POST request with authentication"""
url = f"{self.base_url}/api/{self.api_version}/{endpoint}"
response = self.session.post(url, json=data)
response.raise_for_status()
return response.json()
# Usage
client = BillTrackingAPIClient(
tenant_code=os.getenv('TENANT_CODE'),
api_version='1.0'
)
states = client.get('dropdown/states')Security Best Practices
1. Credential Security
javascript
// DON'T: Never hardcode credentials
const TENANT_CODE = 'ABC123'; // WRONG!
// DO: Use environment variables or secure vaults
const TENANT_CODE = process.env.BILLTRACKING_TENANT_CODE;
// DO: Use configuration management
const config = require('./config');
const TENANT_CODE = config.get('billtracking.tenantCode');2. HTTPS Enforcement
python
# Always verify SSL certificates in production
import requests
# DO: Verify SSL (default behavior)
response = requests.get(url, verify=True)
# DON'T: Never disable SSL verification in production
# response = requests.get(url, verify=False) # INSECURE!
# DO: Use certificate pinning for extra security
response = requests.get(url, verify='/path/to/ca-bundle.crt')3. Rate Limit Handling
javascript
class RateLimitHandler {
constructor() {
this.queue = [];
this.processing = false;
this.rateLimitReset = null;
}
async executeRequest(requestFn) {
return new Promise((resolve, reject) => {
this.queue.push({ requestFn, resolve, reject });
this.processQueue();
});
}
async processQueue() {
if (this.processing || this.queue.length === 0) return;
// Check if we're rate limited
if (this.rateLimitReset && Date.now() < this.rateLimitReset) {
const waitTime = this.rateLimitReset - Date.now();
setTimeout(() => this.processQueue(), waitTime);
return;
}
this.processing = true;
const { requestFn, resolve, reject } = this.queue.shift();
try {
const response = await requestFn();
// Check rate limit headers
const remaining = response.headers['x-ratelimit-remaining'];
if (remaining && parseInt(remaining) === 0) {
this.rateLimitReset = parseInt(response.headers['x-ratelimit-reset']) * 1000;
}
resolve(response);
} catch (error) {
if (error.response?.status === 429) {
// Rate limited - requeue the request
this.queue.unshift({ requestFn, resolve, reject });
this.rateLimitReset = Date.now() + 60000; // Wait 1 minute
} else {
reject(error);
}
} finally {
this.processing = false;
this.processQueue();
}
}
}4. Request Signing (Optional Enhanced Security)
python
import hmac
import hashlib
import base64
from datetime import datetime
def sign_request(method: str, path: str, body: str, secret: str) -> str:
"""Generate request signature for additional security"""
timestamp = datetime.utcnow().isoformat()
# Create signature base string
signature_base = f"{method}\n{path}\n{timestamp}\n{body}"
# Generate HMAC signature
signature = hmac.new(
secret.encode('utf-8'),
signature_base.encode('utf-8'),
hashlib.sha256
).digest()
return base64.b64encode(signature).decode('utf-8')
# Usage in request
headers = {
'api-version': '1.0',
'tenant-code': tenant_code,
'X-Signature': sign_request('POST', '/api/1.0/bill/search', json_body, secret),
'X-Timestamp': datetime.utcnow().isoformat()
}5. IP Whitelisting
If your organization requires IP whitelisting:
javascript
// Provide your static IP addresses during onboarding
const allowedIPs = [
'203.0.113.0/24', // Your office network
'198.51.100.42', // Your production server
'192.0.2.0/24' // Your development network
];
// The API will only accept requests from these IPsAuthentication Examples
JavaScript (Node.js) Full Example
javascript
const axios = require('axios');
const retry = require('axios-retry');
class BillTrackingAPI {
constructor(config) {
this.client = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout || 30000,
headers: {
'api-version': config.apiVersion,
'tenant-code': config.tenantCode,
'Content-Type': 'application/json'
}
});
// Configure automatic retry
retry(this.client, {
retries: 3,
retryDelay: retry.exponentialDelay,
retryCondition: (error) => {
return error.response?.status === 429 || error.response?.status >= 500;
}
});
// Add response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => this.handleError(error)
);
}
handleError(error) {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 401:
throw new Error('Authentication failed: Invalid tenant code');
case 403:
throw new Error('Access denied: Insufficient permissions');
case 429:
throw new Error('Rate limit exceeded: Please retry later');
default:
throw new Error(`API Error ${status}: ${data.message || 'Unknown error'}`);
}
}
throw error;
}
async getStates() {
const response = await this.client.get('/api/1.0/dropdown/states');
return response.data;
}
async searchBills(criteria) {
const response = await this.client.post('/api/1.0/bill/search', criteria);
return response.data;
}
}
// Usage
const api = new BillTrackingAPI({
baseUrl: 'https://engagifii-billtracking.azurewebsites.net',
apiVersion: '1.0',
tenantCode: process.env.TENANT_CODE,
timeout: 30000
});
api.getStates()
.then(states => console.log('States:', states))
.catch(error => console.error('Error:', error.message));Python Full Example
python
import requests
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
import logging
from typing import Dict, Any, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class BillTrackingAPI:
def __init__(self, tenant_code: str, api_version: str = '1.0'):
self.base_url = 'https://engagifii-billtracking.azurewebsites.net'
self.tenant_code = tenant_code
self.api_version = api_version
# Create session with retry strategy
self.session = self._create_session()
def _create_session(self) -> requests.Session:
session = requests.Session()
# Configure retry strategy
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
# Set default headers
session.headers.update({
'api-version': self.api_version,
'tenant-code': self.tenant_code,
'Content-Type': 'application/json',
'User-Agent': 'BillTracking-Python/1.0'
})
return session
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
"""Handle API response and errors"""
try:
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise AuthenticationError("Invalid tenant code or missing authentication")
elif response.status_code == 403:
raise AuthorizationError("Access denied for this resource")
elif response.status_code == 429:
raise RateLimitError("Rate limit exceeded",
retry_after=response.headers.get('Retry-After'))
else:
raise APIError(f"API request failed: {e}")
except ValueError:
raise APIError("Invalid JSON response")
def get(self, endpoint: str, **kwargs) -> Dict[str, Any]:
"""Make GET request"""
url = f"{self.base_url}/api/{self.api_version}/{endpoint}"
logger.info(f"GET {url}")
response = self.session.get(url, **kwargs)
return self._handle_response(response)
def post(self, endpoint: str, data: Optional[Dict] = None, **kwargs) -> Dict[str, Any]:
"""Make POST request"""
url = f"{self.base_url}/api/{self.api_version}/{endpoint}"
logger.info(f"POST {url}")
response = self.session.post(url, json=data, **kwargs)
return self._handle_response(response)
# Custom exceptions
class APIError(Exception):
pass
class AuthenticationError(APIError):
pass
class AuthorizationError(APIError):
pass
class RateLimitError(APIError):
def __init__(self, message, retry_after=None):
super().__init__(message)
self.retry_after = retry_after
# Usage
if __name__ == "__main__":
import os
api = BillTrackingAPI(
tenant_code=os.getenv('TENANT_CODE'),
api_version='1.0'
)
try:
states = api.get('dropdown/states')
print(f"Retrieved {len(states)} states")
except AuthenticationError as e:
print(f"Authentication failed: {e}")
except RateLimitError as e:
print(f"Rate limited: {e}. Retry after {e.retry_after} seconds")
except APIError as e:
print(f"API error: {e}")C# Full Example
csharp
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
using Newtonsoft.Json;
using Polly;
using Polly.Extensions.Http;
public class BillTrackingApiClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly string _baseUrl;
private readonly string _apiVersion;
private readonly string _tenantCode;
public BillTrackingApiClient(string tenantCode, string apiVersion = "1.0")
{
_baseUrl = "https://engagifii-billtracking.azurewebsites.net";
_apiVersion = apiVersion;
_tenantCode = tenantCode;
_httpClient = new HttpClient();
ConfigureHttpClient();
}
private void ConfigureHttpClient()
{
_httpClient.BaseAddress = new Uri(_baseUrl);
_httpClient.Timeout = TimeSpan.FromSeconds(30);
// Add default headers
_httpClient.DefaultRequestHeaders.Add("api-version", _apiVersion);
_httpClient.DefaultRequestHeaders.Add("tenant-code", _tenantCode);
_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {timespan} seconds");
});
}
public async Task<T> GetAsync<T>(string endpoint)
{
var policy = GetRetryPolicy();
var response = await policy.ExecuteAsync(async () =>
await _httpClient.GetAsync($"/api/{_apiVersion}/{endpoint}")
);
await HandleResponseErrors(response);
var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(content);
}
public async Task<T> PostAsync<T>(string endpoint, object data)
{
var policy = GetRetryPolicy();
var json = JsonConvert.SerializeObject(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await policy.ExecuteAsync(async () =>
await _httpClient.PostAsync($"/api/{_apiVersion}/{endpoint}", content)
);
await HandleResponseErrors(response);
var responseContent = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(responseContent);
}
private async Task HandleResponseErrors(HttpResponseMessage response)
{
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
switch (response.StatusCode)
{
case System.Net.HttpStatusCode.Unauthorized:
throw new UnauthorizedAccessException("Invalid tenant code or missing authentication");
case System.Net.HttpStatusCode.Forbidden:
throw new UnauthorizedAccessException("Access denied for this resource");
case System.Net.HttpStatusCode.TooManyRequests:
throw new Exception("Rate limit exceeded. Please retry later");
default:
throw new HttpRequestException($"API request failed: {response.StatusCode} - {errorContent}");
}
}
}
public void Dispose()
{
_httpClient?.Dispose();
}
}
// Usage
class Program
{
static async Task Main(string[] args)
{
var tenantCode = Environment.GetEnvironmentVariable("TENANT_CODE");
using (var apiClient = new BillTrackingApiClient(tenantCode))
{
try
{
var states = await apiClient.GetAsync<List<State>>("dropdown/states");
Console.WriteLine($"Retrieved {states.Count} states");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Authentication error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}Error Scenarios
Authentication Failure Responses
401 Unauthorized
json
{
"error": "Unauthorized",
"message": "Missing or invalid authentication headers",
"details": {
"missing_headers": ["tenant-code"],
"timestamp": "2025-01-28T10:30:00Z"
}
}Resolution Steps:
- Verify both
api-versionandtenant-codeheaders are present - Check header values are correct and not empty
- Ensure no typos in header names
- Verify tenant code is active and not expired
403 Forbidden
json
{
"error": "Forbidden",
"message": "Access denied for this tenant",
"details": {
"tenant": "TENANT-ABC123",
"resource": "/api/1.0/admin/settings",
"timestamp": "2025-01-28T10:30:00Z"
}
}Resolution Steps:
- Check if your tenant has access to the requested endpoint
- Verify your tenant's permission scope
- Contact support if you believe you should have access
Rate Limiting Responses
429 Too Many Requests
json
{
"error": "TooManyRequests",
"message": "Rate limit exceeded",
"details": {
"limit": 1000,
"remaining": 0,
"reset": 1706441400,
"retry_after": 60
}
}Response Headers:
http
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706441400
Retry-After: 60Resolution Steps:
- Implement exponential backoff
- Respect the
Retry-Afterheader - Cache frequently accessed data
- Consider request batching where possible
Common Error Patterns and Solutions
javascript
// Comprehensive error handler
function handleApiError(error) {
if (!error.response) {
// Network error
console.error('Network error:', error.message);
return { retry: true, delay: 5000 };
}
const { status, data, headers } = error.response;
switch (status) {
case 400:
console.error('Bad request:', data.message);
return { retry: false, error: 'Invalid request parameters' };
case 401:
console.error('Authentication failed');
return { retry: false, error: 'Check your tenant code' };
case 403:
console.error('Access denied');
return { retry: false, error: 'Insufficient permissions' };
case 429:
const retryAfter = headers['retry-after'] || 60;
console.warn(`Rate limited. Retry after ${retryAfter}s`);
return { retry: true, delay: retryAfter * 1000 };
case 500:
case 502:
case 503:
case 504:
console.error('Server error:', status);
return { retry: true, delay: 10000 };
default:
console.error(`Unexpected error ${status}:`, data);
return { retry: false, error: data.message };
}
}Next Step: Explore the API Reference for detailed endpoint documentation
