Skip to content

Authentication & Security

The Engagifii Revenue API implements OAuth 2.0 authentication with additional security layers to protect sensitive financial data. This guide covers authentication flows, security best practices, and implementation details.

Table of Contents

  1. Authentication Methods
  2. OAuth 2.0 Flow
  3. Token Management
  4. API Key Authentication
  5. Security Headers
  6. Rate Limiting
  7. Security Best Practices
  8. Error Scenarios
  9. Code Examples

Authentication Methods

The Revenue API supports multiple authentication methods:

MethodUse CaseSecurity LevelToken Lifetime
OAuth 2.0 Client CredentialsService-to-service integrationHigh1 hour
OAuth 2.0 Authorization CodeUser delegationHigh1 hour
API Key + SecretLegacy integrationsMediumNo expiry
JWT Bearer TokenModern microservicesHighConfigurable

OAuth 2.0 Flow

The Client Credentials flow is ideal for server-to-server authentication where no user interaction is required.

Step 1: Register Your Application

Contact your account manager to register your application and receive:

  • client_id: Your application's unique identifier
  • client_secret: Your application's secret key
  • tenant_code: Your tenant identifier

Step 2: Request Access Token

Endpoint: POST /oauth/token

bash
curl -X POST https://engagifii-prod-revenue.azurewebsites.net/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "scope=revenue.api.full"

Request Parameters:

ParameterRequiredDescription
grant_typeYesMust be client_credentials
client_idYesYour application ID
client_secretYesYour application secret
scopeNoPermission scope (default: revenue.api.full)

Success Response:

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "revenue.api.full",
  "refresh_token": "optional_refresh_token"
}

Step 3: Use Access Token

Include the access token in the Authorization header:

http
Authorization: Bearer YOUR_ACCESS_TOKEN

Authorization Code Flow

For applications that act on behalf of users:

Step 1: Redirect User for Authorization

https://engagifii-prod-revenue.azurewebsites.net/oauth/authorize?
  response_type=code&
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://yourapp.com/callback&
  scope=revenue.api.read revenue.api.write&
  state=RANDOM_STATE_STRING

Step 2: Handle Callback

After user authorization, handle the callback:

javascript
// Extract authorization code from URL
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');
const state = urlParams.get('state');

// Verify state to prevent CSRF attacks
if (state !== savedState) {
  throw new Error('Invalid state parameter');
}

Step 3: Exchange Code for Token

bash
curl -X POST https://engagifii-prod-revenue.azurewebsites.net/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "redirect_uri=https://yourapp.com/callback"

Token Management

Token Storage

DO:

  • Store tokens in secure, encrypted storage
  • Use environment variables for credentials
  • Implement token encryption at rest

DON'T:

  • Store tokens in plain text files
  • Include tokens in source code
  • Log tokens in application logs

Token Refresh

Implement automatic token refresh before expiration:

javascript
class TokenManager {
  constructor(clientId, clientSecret) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.token = null;
    this.tokenExpiry = null;
  }
  
  async getToken() {
    // Refresh if token expires in less than 5 minutes
    if (!this.token || Date.now() >= this.tokenExpiry - 300000) {
      await this.refreshToken();
    }
    return this.token;
  }
  
  async refreshToken() {
    const response = await fetch('https://engagifii-prod-revenue.azurewebsites.net/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret,
        scope: 'revenue.api.full'
      })
    });
    
    const data = await response.json();
    this.token = data.access_token;
    this.tokenExpiry = Date.now() + (data.expires_in * 1000);
    
    console.log('Token refreshed, expires at:', new Date(this.tokenExpiry));
  }
}

Token Revocation

Revoke tokens when no longer needed:

bash
curl -X POST https://engagifii-prod-revenue.azurewebsites.net/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=YOUR_ACCESS_TOKEN" \
  -d "token_type_hint=access_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

API Key Authentication

For legacy systems or simple integrations:

Obtaining API Keys

  1. Log into the Engagifii dashboard
  2. Navigate to Settings → API Keys
  3. Generate a new API key pair
  4. Store the secret securely (shown only once)

Using API Keys

Include API key and secret in headers:

http
X-API-Key: YOUR_API_KEY
X-API-Secret: YOUR_API_SECRET
tenant-code: YOUR_TENANT_CODE
api-version: 1.0

Example Request:

bash
curl -X GET https://engagifii-prod-revenue.azurewebsites.net/api/1.0/invoice \
  -H "X-API-Key: pk_live_abc123" \
  -H "X-API-Secret: sk_live_xyz789" \
  -H "tenant-code: TENANT123" \
  -H "api-version: 1.0"

Security Headers

Required Headers

All requests must include:

HeaderDescriptionExample
AuthorizationBearer token or API credentialsBearer eyJhbG...
tenant-codeYour tenant identifierTENANT123
api-versionAPI version1.0
Content-TypeRequest content typeapplication/json

Optional Security Headers

HeaderDescriptionExample
X-Request-IDUnique request identifier for tracinguuid-v4
X-Idempotency-KeyPrevent duplicate operationsunique-key
X-Forwarded-ForOriginal client IP (if behind proxy)203.0.113.0

Rate Limiting

Rate Limit Headers

All responses include rate limit information:

http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1609459200
X-RateLimit-Reset-After: 120

Handling Rate Limits

Implement exponential backoff:

python
import time
import random

def make_api_call_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)
        
        if response.status_code == 429:  # Too Many Requests
            retry_after = int(response.headers.get('X-RateLimit-Reset-After', 60))
            jitter = random.uniform(0, 1)  # Add jitter to prevent thundering herd
            sleep_time = retry_after + jitter
            
            print(f"Rate limited. Waiting {sleep_time} seconds...")
            time.sleep(sleep_time)
            continue
            
        return response
    
    raise Exception("Max retries exceeded")

Security Best Practices

1. Credential Management

javascript
// Good: Use environment variables
const config = {
  clientId: process.env.ENGAGIFII_CLIENT_ID,
  clientSecret: process.env.ENGAGIFII_CLIENT_SECRET,
  tenantCode: process.env.ENGAGIFII_TENANT_CODE
};

// Bad: Hardcoded credentials
const config = {
  clientId: 'abc123',  // Never do this!
  clientSecret: 'xyz789',  // Security risk!
  tenantCode: 'TENANT123'
};

2. HTTPS Only

Always use HTTPS for API calls:

javascript
// Good
const apiUrl = 'https://engagifii-prod-revenue.azurewebsites.net/api/1.0/';

// Bad - Never use HTTP
const apiUrl = 'http://engagifii-prod-revenue.azurewebsites.net/api/1.0/';

3. IP Whitelisting

For production environments, configure IP whitelisting:

  1. Contact support to enable IP whitelisting
  2. Provide static IP addresses or CIDR ranges
  3. Test from whitelisted IPs before go-live

4. Audit Logging

Log all API interactions for security auditing:

python
import logging
import json
from datetime import datetime

class APIAuditLogger:
    def __init__(self):
        self.logger = logging.getLogger('api_audit')
        
    def log_request(self, method, endpoint, user_id=None):
        audit_entry = {
            'timestamp': datetime.utcnow().isoformat(),
            'method': method,
            'endpoint': endpoint,
            'user_id': user_id,
            'ip_address': self.get_client_ip(),
            'action': 'api_request'
        }
        self.logger.info(json.dumps(audit_entry))
        
    def log_response(self, status_code, response_time):
        audit_entry = {
            'timestamp': datetime.utcnow().isoformat(),
            'status_code': status_code,
            'response_time_ms': response_time,
            'action': 'api_response'
        }
        self.logger.info(json.dumps(audit_entry))

5. Token Security

javascript
// Secure token storage with encryption
const crypto = require('crypto');

class SecureTokenStorage {
  constructor(encryptionKey) {
    this.algorithm = 'aes-256-gcm';
    this.key = Buffer.from(encryptionKey, 'hex');
  }
  
  encrypt(token) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
    
    let encrypted = cipher.update(token, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag();
    
    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  }
  
  decrypt(encryptedData) {
    const decipher = crypto.createDecipheriv(
      this.algorithm,
      this.key,
      Buffer.from(encryptedData.iv, 'hex')
    );
    
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
    
    let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  }
}

Error Scenarios

Authentication Errors

401 Unauthorized

Causes:

  • Invalid or expired token
  • Missing authentication headers
  • Incorrect credentials

Response:

json
{
  "error": "unauthorized",
  "error_description": "The access token is invalid or has expired",
  "error_uri": "https://docs.engagifii.com/errors/unauthorized"
}

Solution:

javascript
async function apiCallWithAuth(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    if (response.status === 401) {
      // Refresh token and retry
      const newToken = await refreshAccessToken();
      options.headers.Authorization = `Bearer ${newToken}`;
      return fetch(url, options);
    }
    
    return response;
  } catch (error) {
    console.error('API call failed:', error);
    throw error;
  }
}

403 Forbidden

Causes:

  • Insufficient permissions
  • IP not whitelisted
  • Tenant mismatch

Response:

json
{
  "error": "forbidden",
  "error_description": "You do not have permission to access this resource",
  "required_scope": "revenue.api.write"
}

Token Errors

Invalid Grant

Response:

json
{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant is invalid, expired, revoked, or does not match the redirection URI"
}

Solution:

  • Verify client credentials
  • Check if client is active
  • Ensure redirect URI matches registration

Invalid Scope

Response:

json
{
  "error": "invalid_scope",
  "error_description": "The requested scope is invalid, unknown, or malformed",
  "available_scopes": ["revenue.api.read", "revenue.api.write"]
}

Code Examples

Complete Authentication Implementation

Node.js with Axios

javascript
const axios = require('axios');

class EngagifiiAPIClient {
  constructor(config) {
    this.baseURL = 'https://engagifii-prod-revenue.azurewebsites.net';
    this.clientId = config.clientId;
    this.clientSecret = config.clientSecret;
    this.tenantCode = config.tenantCode;
    this.token = null;
    this.tokenExpiry = null;
    
    // Create axios instance with defaults
    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/json',
        'tenant-code': this.tenantCode,
        'api-version': '1.0'
      }
    });
    
    // Add request interceptor for authentication
    this.client.interceptors.request.use(
      async (config) => {
        const token = await this.getValidToken();
        config.headers.Authorization = `Bearer ${token}`;
        return config;
      },
      (error) => Promise.reject(error)
    );
    
    // Add response interceptor for error handling
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error.response?.status === 401) {
          // Token expired, refresh and retry
          this.token = null;
          const originalRequest = error.config;
          const token = await this.getValidToken();
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return this.client(originalRequest);
        }
        return Promise.reject(error);
      }
    );
  }
  
  async getValidToken() {
    if (!this.token || Date.now() >= this.tokenExpiry - 60000) {
      await this.refreshToken();
    }
    return this.token;
  }
  
  async refreshToken() {
    try {
      const response = await axios.post(`${this.baseURL}/oauth/token`, 
        new URLSearchParams({
          grant_type: 'client_credentials',
          client_id: this.clientId,
          client_secret: this.clientSecret,
          scope: 'revenue.api.full'
        }), {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        }
      );
      
      this.token = response.data.access_token;
      this.tokenExpiry = Date.now() + (response.data.expires_in * 1000);
      
      console.log('Token refreshed successfully');
    } catch (error) {
      console.error('Failed to refresh token:', error.response?.data || error.message);
      throw error;
    }
  }
  
  // API Methods
  async getInvoice(invoiceId) {
    const response = await this.client.get(`/api/1.0/invoice/${invoiceId}`);
    return response.data;
  }
  
  async createInvoice(invoiceData) {
    const response = await this.client.post('/api/1.0/invoice', invoiceData);
    return response.data;
  }
  
  async listInvoices(params = {}) {
    const response = await this.client.get('/api/1.0/invoice', { params });
    return response.data;
  }
}

// Usage
const client = new EngagifiiAPIClient({
  clientId: process.env.ENGAGIFII_CLIENT_ID,
  clientSecret: process.env.ENGAGIFII_CLIENT_SECRET,
  tenantCode: process.env.ENGAGIFII_TENANT_CODE
});

// Make API calls
client.getInvoice('INV-123456')
  .then(invoice => console.log('Invoice:', invoice))
  .catch(error => console.error('Error:', error));

Python with Requests

python
import os
import time
import requests
from datetime import datetime, timedelta
from typing import Optional, Dict, Any

class EngagifiiAPIClient:
    def __init__(self, client_id: str, client_secret: str, tenant_code: str):
        self.base_url = "https://engagifii-prod-revenue.azurewebsites.net"
        self.client_id = client_id
        self.client_secret = client_secret
        self.tenant_code = tenant_code
        self.token = None
        self.token_expiry = None
        
        self.session = requests.Session()
        self.session.headers.update({
            'Content-Type': 'application/json',
            'tenant-code': self.tenant_code,
            'api-version': '1.0'
        })
    
    def _get_valid_token(self) -> str:
        """Get a valid access token, refreshing if necessary."""
        if not self.token or datetime.now() >= self.token_expiry - timedelta(minutes=1):
            self._refresh_token()
        return self.token
    
    def _refresh_token(self):
        """Refresh the access token."""
        response = requests.post(
            f"{self.base_url}/oauth/token",
            data={
                'grant_type': 'client_credentials',
                'client_id': self.client_id,
                'client_secret': self.client_secret,
                'scope': 'revenue.api.full'
            },
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )
        
        if response.status_code != 200:
            raise Exception(f"Failed to refresh token: {response.text}")
        
        data = response.json()
        self.token = data['access_token']
        self.token_expiry = datetime.now() + timedelta(seconds=data['expires_in'])
        
        print(f"Token refreshed, expires at {self.token_expiry}")
    
    def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
        """Make an authenticated API request."""
        url = f"{self.base_url}/api/1.0/{endpoint}"
        
        # Add authentication
        token = self._get_valid_token()
        self.session.headers['Authorization'] = f"Bearer {token}"
        
        # Make request with retry logic
        max_retries = 3
        for attempt in range(max_retries):
            response = self.session.request(method, url, **kwargs)
            
            if response.status_code == 401:
                # Token might be invalid, refresh and retry
                self.token = None
                token = self._get_valid_token()
                self.session.headers['Authorization'] = f"Bearer {token}"
                continue
            
            if response.status_code == 429:
                # Rate limited, wait and retry
                retry_after = int(response.headers.get('X-RateLimit-Reset-After', 60))
                print(f"Rate limited, waiting {retry_after} seconds...")
                time.sleep(retry_after)
                continue
            
            response.raise_for_status()
            return response.json()
        
        raise Exception(f"Max retries exceeded for {endpoint}")
    
    # API Methods
    def get_invoice(self, invoice_id: str) -> Dict[str, Any]:
        """Get invoice details."""
        return self._make_request('GET', f'invoice/{invoice_id}')
    
    def create_invoice(self, invoice_data: Dict[str, Any]) -> Dict[str, Any]:
        """Create a new invoice."""
        return self._make_request('POST', 'invoice', json=invoice_data)
    
    def list_invoices(self, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """List invoices with optional filters."""
        return self._make_request('GET', 'invoice', params=params)
    
    def process_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
        """Process a payment."""
        return self._make_request('POST', 'payment', json=payment_data)

# Usage
if __name__ == "__main__":
    client = EngagifiiAPIClient(
        client_id=os.getenv('ENGAGIFII_CLIENT_ID'),
        client_secret=os.getenv('ENGAGIFII_CLIENT_SECRET'),
        tenant_code=os.getenv('ENGAGIFII_TENANT_CODE')
    )
    
    try:
        # List recent invoices
        invoices = client.list_invoices({'status': 'pending', 'limit': 10})
        print(f"Found {len(invoices['items'])} pending invoices")
        
        # Get specific invoice
        if invoices['items']:
            invoice = client.get_invoice(invoices['items'][0]['id'])
            print(f"Invoice {invoice['invoiceNumber']}: ${invoice['totalAmount']}")
    
    except Exception as e:
        print(f"Error: {e}")

C# with HttpClient

csharp
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;

public class EngagifiiAPIClient
{
    private readonly HttpClient httpClient;
    private readonly string clientId;
    private readonly string clientSecret;
    private readonly string tenantCode;
    private string accessToken;
    private DateTime tokenExpiry;
    
    public EngagifiiAPIClient(string clientId, string clientSecret, string tenantCode)
    {
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tenantCode = tenantCode;
        
        httpClient = new HttpClient
        {
            BaseAddress = new Uri("https://engagifii-prod-revenue.azurewebsites.net")
        };
        
        httpClient.DefaultRequestHeaders.Add("tenant-code", tenantCode);
        httpClient.DefaultRequestHeaders.Add("api-version", "1.0");
        httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }
    
    private async Task<string> GetValidTokenAsync()
    {
        if (string.IsNullOrEmpty(accessToken) || DateTime.UtcNow >= tokenExpiry.AddMinutes(-1))
        {
            await RefreshTokenAsync();
        }
        return accessToken;
    }
    
    private async Task RefreshTokenAsync()
    {
        var tokenRequest = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "client_credentials"),
            new KeyValuePair<string, string>("client_id", clientId),
            new KeyValuePair<string, string>("client_secret", clientSecret),
            new KeyValuePair<string, string>("scope", "revenue.api.full")
        });
        
        var response = await httpClient.PostAsync("/oauth/token", tokenRequest);
        response.EnsureSuccessStatusCode();
        
        var content = await response.Content.ReadAsStringAsync();
        dynamic tokenResponse = JsonConvert.DeserializeObject(content);
        
        accessToken = tokenResponse.access_token;
        int expiresIn = tokenResponse.expires_in;
        tokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn);
        
        Console.WriteLine($"Token refreshed, expires at {tokenExpiry}");
    }
    
    private async Task<T> MakeRequestAsync<T>(HttpMethod method, string endpoint, object data = null)
    {
        var token = await GetValidTokenAsync();
        
        var request = new HttpRequestMessage(method, $"/api/1.0/{endpoint}");
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        
        if (data != null)
        {
            request.Content = new StringContent(
                JsonConvert.SerializeObject(data),
                Encoding.UTF8,
                "application/json");
        }
        
        var response = await httpClient.SendAsync(request);
        
        if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            // Token might be invalid, refresh and retry
            accessToken = null;
            token = await GetValidTokenAsync();
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
            response = await httpClient.SendAsync(request);
        }
        
        response.EnsureSuccessStatusCode();
        
        var responseContent = await response.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(responseContent);
    }
    
    // API Methods
    public async Task<Invoice> GetInvoiceAsync(string invoiceId)
    {
        return await MakeRequestAsync<Invoice>(HttpMethod.Get, $"invoice/{invoiceId}");
    }
    
    public async Task<Invoice> CreateInvoiceAsync(CreateInvoiceRequest invoiceData)
    {
        return await MakeRequestAsync<Invoice>(HttpMethod.Post, "invoice", invoiceData);
    }
    
    public async Task<InvoiceList> ListInvoicesAsync(InvoiceFilter filter = null)
    {
        var query = filter != null ? $"?{filter.ToQueryString()}" : "";
        return await MakeRequestAsync<InvoiceList>(HttpMethod.Get, $"invoice{query}");
    }
}

// Usage
class Program
{
    static async Task Main(string[] args)
    {
        var client = new EngagifiiAPIClient(
            Environment.GetEnvironmentVariable("ENGAGIFII_CLIENT_ID"),
            Environment.GetEnvironmentVariable("ENGAGIFII_CLIENT_SECRET"),
            Environment.GetEnvironmentVariable("ENGAGIFII_TENANT_CODE")
        );
        
        try
        {
            // Get invoice
            var invoice = await client.GetInvoiceAsync("INV-123456");
            Console.WriteLine($"Invoice {invoice.InvoiceNumber}: ${invoice.TotalAmount}");
            
            // List invoices
            var invoices = await client.ListInvoicesAsync(new InvoiceFilter 
            { 
                Status = "pending",
                Limit = 10 
            });
            Console.WriteLine($"Found {invoices.TotalCount} pending invoices");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

For additional security information and best practices, refer to our Security White Paper or contact our security team at security@engagifii.com.