Skip to content

Authentication & Security

Table of Contents

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:

HeaderTypeDescriptionExample
api-versionStringAPI version identifier1.0
tenant-codeStringUnique organization identifierTENANT-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/json

Authentication 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 error

Token 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 code

Step 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 IPs

Authentication 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:

  1. Verify both api-version and tenant-code headers are present
  2. Check header values are correct and not empty
  3. Ensure no typos in header names
  4. 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:

  1. Check if your tenant has access to the requested endpoint
  2. Verify your tenant's permission scope
  3. 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: 60

Resolution Steps:

  1. Implement exponential backoff
  2. Respect the Retry-After header
  3. Cache frequently accessed data
  4. 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