Appearance
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
- Authentication Methods
- OAuth 2.0 Flow
- Token Management
- API Key Authentication
- Security Headers
- Rate Limiting
- Security Best Practices
- Error Scenarios
- Code Examples
Authentication Methods
The Revenue API supports multiple authentication methods:
| Method | Use Case | Security Level | Token Lifetime |
|---|---|---|---|
| OAuth 2.0 Client Credentials | Service-to-service integration | High | 1 hour |
| OAuth 2.0 Authorization Code | User delegation | High | 1 hour |
| API Key + Secret | Legacy integrations | Medium | No expiry |
| JWT Bearer Token | Modern microservices | High | Configurable |
OAuth 2.0 Flow
Client Credentials Flow (Recommended)
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 identifierclient_secret: Your application's secret keytenant_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:
| Parameter | Required | Description |
|---|---|---|
grant_type | Yes | Must be client_credentials |
client_id | Yes | Your application ID |
client_secret | Yes | Your application secret |
scope | No | Permission 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_TOKENAuthorization 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_STRINGStep 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
- Log into the Engagifii dashboard
- Navigate to Settings → API Keys
- Generate a new API key pair
- 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.0Example 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:
| Header | Description | Example |
|---|---|---|
Authorization | Bearer token or API credentials | Bearer eyJhbG... |
tenant-code | Your tenant identifier | TENANT123 |
api-version | API version | 1.0 |
Content-Type | Request content type | application/json |
Optional Security Headers
| Header | Description | Example |
|---|---|---|
X-Request-ID | Unique request identifier for tracing | uuid-v4 |
X-Idempotency-Key | Prevent duplicate operations | unique-key |
X-Forwarded-For | Original 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: 120Handling 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:
- Contact support to enable IP whitelisting
- Provide static IP addresses or CIDR ranges
- 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.
