CallRail - Authentication
๐ Overviewโ
CallRail authentication uses API keys generated from the CallRail dashboard. Keys are stored per DashClicks account in MongoDB and used for all API requests. The system supports soft deletion to preserve keys when campaigns are active, and includes automatic token invalidation detection.
Source Files:
- Controller:
external/Integrations/Callrail/Controllers/auth.js - Model:
external/Integrations/Callrail/Models/keys.js - Routes:
external/Integrations/Callrail/Routes/auth.js
External API: N/A (credential storage and management only)
๐๏ธ Collections Usedโ
integrations.callrail.keyโ
- Operations: Create, Read, Update, Delete
- Model:
shared/models/callrail-key.js - Usage Context: Store CallRail API keys with account association
Document Structure:
{
"_id": ObjectId("507f1f77bcf86cd799439011"),
"account_id": ObjectId("507f1f77bcf86cd799439012"),
"api_key": "dfc7f89eaa8a9b4f0acbcd850a60a444",
"nickname": "Production CallRail",
"token_invalidated": false,
"deleted": ISODate("2023-10-01T12:00:00Z"), // Soft delete timestamp
"createdAt": ISODate("2023-09-15T10:00:00Z"),
"updatedAt": ISODate("2023-10-01T12:00:00Z")
}
analytics.callrail.userconfigโ
- Operations: Create, Read, Update, Delete
- Model:
shared/models/analytics-callrail-userconfig.js - Usage Context: Link CallRail account/company to DashClicks account for campaign tracking
Document Structure:
{
"_id": ObjectId,
"account_id": ObjectId,
"token": "507f1f77bcf86cd799439011", // Reference to callrail.key document ID
"callrail_acountid": "470306151",
"callrail_companyid": "com_abc123",
"createdAt": ISODate,
"updatedAt": ISODate
}
๐ Data Flowโ
API Key Registration Flowโ
sequenceDiagram
participant Client as DashClicks Frontend
participant Controller as Auth Controller
participant KeysDB as MongoDB (callrail.key)
Client->>Controller: POST /v1/e/callrail/auth {api_key, nickname}
Controller->>KeysDB: Find existing key for account
alt Key exists and valid
KeysDB-->>Controller: Return existing key
Controller-->>Client: Return existing key document
else Key invalidated
KeysDB-->>Controller: Return invalidated key
Controller->>KeysDB: Delete invalidated key
Controller->>KeysDB: Save new key
KeysDB-->>Controller: New key document
Controller-->>Client: Return new key
else No key exists
KeysDB-->>Controller: No document found
Controller->>KeysDB: Save new key
KeysDB-->>Controller: Key document
Controller-->>Client: Return key document
end
User Analytics Config Flowโ
sequenceDiagram
participant Client as DashClicks Frontend
participant Controller as Auth Controller
participant UserDB as MongoDB (User)
participant ConfigDB as MongoDB (analytics.callrail.userconfig)
participant FCM as FCM Service
Client->>Controller: POST /v1/e/callrail/auth/config
Controller->>UserDB: Find account owner
UserDB-->>Controller: Owner user document
Controller->>ConfigDB: Upsert config (account_id)
ConfigDB-->>Controller: Config saved
Controller->>FCM: Send notification (project added)
FCM-->>Controller: Notification sent
Controller-->>Client: {success: true, data: config}
๐ง Business Logic & Functionsโ
Controller Functionsโ
postApiKey(req, res, next)โ
Purpose: Save or retrieve CallRail API key for account
Source: Controllers/auth.js
External API Endpoint: N/A (database operation only)
Parameters:
req.body.api_key(String) - CallRail API keyreq.body.nickname(String) - Friendly name for the keyreq.auth.account_id(ObjectId) - Account ID from JWT
Returns: JSON response with key document
{
"success": true,
"message": "SUCCESS",
"data": {
"id": "507f1f77bcf86cd799439011",
"account_id": "507f1f77bcf86cd799439012",
"api_key": "dfc7f89eaa8a9b4f0acbcd850a60a444",
"nickname": "Production CallRail"
}
}
Business Logic Flow:
-
Validate Account Access
- Call
checkAccountAccess(req)to verify account permissions - Return error if invalid account ID
- Call
-
Check Existing Key
- Query database for existing key by account_id
- Exclude soft-deleted keys (
deleted: { $exists: false })
-
Handle Existing Valid Key
- If key exists and not invalidated, return existing key
- Avoid creating duplicate keys
-
Handle Invalidated Key
- If key exists but
token_invalidated: true - Delete all keys for account
- Proceed to save new key
- If key exists but
-
Validate Required Fields
- Check api_key and nickname provided
- Return 400 error if missing
-
Save New Key
- Create document with api_key, account_id, nickname
- Return saved document with generated ID
Request Example:
POST /v1/e/callrail/auth
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"api_key": "dfc7f89eaa8a9b4f0acbcd850a60a444",
"nickname": "Production CallRail"
}
Success Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"id": "507f1f77bcf86cd799439011",
"account_id": "507f1f77bcf86cd799439012",
"api_key": "dfc7f89eaa8a9b4f0acbcd850a60a444",
"nickname": "Production CallRail",
"createdAt": "2023-10-01T12:00:00.000Z",
"updatedAt": "2023-10-01T12:00:00.000Z"
}
}
Error Response (Missing Fields):
{
"success": false,
"errno": 400,
"message": "API Key and nickname are required.",
"code": "API_KEY_NICKNAME_REQUIRED"
}
Error Handling:
- Invalid Account ID: Returns 400 with
INVALID_ACCOUNT_ID - Missing api_key or nickname: Returns 400 with custom error
- Database Errors: Propagated to error middleware
Example Usage:
// Save CallRail API key
await fetch('/v1/e/callrail/auth', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
api_key: 'dfc7f89eaa8a9b4f0acbcd850a60a444',
nickname: 'Production CallRail',
}),
});
Side Effects:
- โ ๏ธ Database Write: Creates new API key document
- โ ๏ธ Key Deletion: Deletes invalidated keys before creating new one
- โน๏ธ Idempotent: Returns existing valid key without creating duplicate
saveUserAnalyticsConfig(req, res, next)โ
Purpose: Save user-specific CallRail configuration for analytics integration
Source: Controllers/auth.js
External API Endpoint: N/A (database operation only)
Parameters:
req.body.token(String) - CallRail key document IDreq.body.account_id(String) - CallRail account IDreq.body.company_id(String) - CallRail company IDreq.auth.account_id(ObjectId) - DashClicks account ID
Returns: JSON response with config document
{
"success": true,
"message": "SUCCESS",
"data": {
"acknowledged": true,
"modifiedCount": 1,
"upsertedId": null,
"upsertedCount": 0,
"matchedCount": 1
}
}
Business Logic Flow:
-
Validate Account Access
- Verify account permissions
-
Find Account Owner
- Query User collection for account owner
- Get owner's user_id for notifications
-
Check Notification Preferences
- Get user config for projects module
- Check if project_added notifications enabled
-
Validate Required Fields
- Verify token, account_id, company_id provided
- Return error if any missing
-
Upsert Configuration
- Update or insert config document
- Link CallRail account/company to DashClicks account
-
Send Notifications
- If enabled, send FCM notification about integration added
- Notification includes bell and browser types
-
Return Success Response
Request Example:
POST /v1/e/callrail/auth/config
Authorization: Bearer {jwt_token}
Content-Type: application/json
{
"token": "507f1f77bcf86cd799439011",
"account_id": "470306151",
"company_id": "com_abc123"
}
Success Response:
{
"success": true,
"message": "SUCCESS",
"data": {
"acknowledged": true,
"modifiedCount": 1
}
}
Error Response (Missing Fields):
{
"success": false,
"errno": 400,
"message": "account id, company_id and token are required."
}
Notification Sent:
{
"title": "Callrail integration added",
"body": "A new Callrail integration has been added.",
"data": { "subType": "bell" },
"module": "projects",
"type": "project_added"
}
Error Handling:
- Invalid Account: Returns 400 with
INVALID_ACCOUNT_ID - Missing Fields: Returns 400 with descriptive error
- Notification Failure: Logged but doesn't fail request
- Database Errors: Returns 400 status
Side Effects:
- โ ๏ธ Database Upsert: Creates or updates config document
- โ ๏ธ FCM Notification: Sends push notification to account owner
- โน๏ธ Idempotent: Upsert ensures only one config per account
deleteApiKey(req, res, next)โ
Purpose: Delete CallRail API key with campaign preservation logic
Source: Controllers/auth.js
External API Endpoint: N/A (database operations only)
Parameters:
req.query.global(String, optional) - "true" for complete removalreq.auth.account_id(ObjectId) - Account IDreq.auth.uid(ObjectId) - User IDreq.auth.parent_account(ObjectId, optional) - Parent account for sub-accounts
Returns: JSON response
{
"success": true,
"message": "SUCCESS"
}
Business Logic Flow:
-
Validate Account Access
- Verify account permissions
-
Find Account Owner and Preferences
- Get account owner user document
- Get user notification preferences for analytics
-
Check Global Delete Flag
If
global=true(Complete Removal):a. Disable Active Campaigns
- Find all active CallRail campaigns
- Mark campaigns as deleted (
is_deleted: true)
b. Delete User Config
- Remove analytics.callrail.userconfig document
c. Soft Delete API Key
- Update key with
deleted: new Date() - Key remains in database but marked deleted
d. Send Notifications
- FCM notification (browser/bell if enabled)
- Email notification (if enabled)
- Send to parent account if sub-account
If
global=falseor not provided (Conditional Delete):a. Check Existing Campaigns
- Query for active CallRail campaigns
b. If Campaigns Exist
- Only delete user config
- Keep API key intact for existing campaigns
- Send notifications
c. If No Campaigns
- Delete user config
- Soft delete API key
- Send notifications
-
Return Success Response
Request Examples:
# Complete removal (delete everything)
DELETE /v1/e/callrail/auth?global=true
Authorization: Bearer {jwt_token}
# Conditional removal (preserve if campaigns exist)
DELETE /v1/e/callrail/auth
Authorization: Bearer {jwt_token}
Success Response:
{
"success": true,
"message": "SUCCESS"
}
Notification Examples:
FCM Notification:
{
"title": "CallRail integration is Disconnected",
"body": "CallRail integration has been disconnected.",
"data": { "subType": "bell" },
"module": "analytics",
"type": "integration_disconnected"
}
Email Notification:
{
"subject": "CallRail integration disconnected",
"content": "CallRail integration has been disconnected.",
"origin": "analytics",
"recipients": [{
"name": "John Doe",
"email": "john@example.com",
"first_name": "John",
"last_name": "Doe"
}]
}
Error Handling:
- Invalid Account: Returns 400 with
INVALID_ACCOUNT_ID - Database Errors: Passed to error middleware
- Notification Failures: Logged but don't fail request
Side Effects:
- โ ๏ธ Campaign Updates: May mark campaigns as deleted
- โ ๏ธ Config Deletion: Removes user config document
- โ ๏ธ Soft Delete: Marks API key as deleted (not removed)
- โ ๏ธ Notifications: Sends email and FCM notifications
- โ ๏ธ Parent Account: May send notifications to parent account
Model Functionsโ
save(authData)โ
Purpose: Save new API key document
Source: Models/keys.js
Parameters:
authData(Object) - Key data to saveapi_key(String) - CallRail API keyaccount_id(ObjectId) - Account IDnickname(String) - Key nickname
Returns: Promise<Object> - Saved document with ID
{
"id": "507f1f77bcf86cd799439011",
"account_id": "507f1f77bcf86cd799439012",
"api_key": "dfc7f89eaa8a9b4f0acbcd850a60a444",
"nickname": "Production CallRail"
}
Example Usage:
const authData = {
api_key: 'dfc7f89eaa8a9b4f0acbcd850a60a444',
account_id: new mongoose.Types.ObjectId(accountId),
nickname: 'Production CallRail',
};
const result = await keysModel.save(authData);
// Returns: { id: '...', api_key: '...', ... }
Side Effects:
- โ ๏ธ Database Write: Creates new document in callrail.key collection
findApiKey(condition)โ
Purpose: Find API key document by conditions
Source: Models/keys.js
Parameters:
condition(Object) - Query conditionsaccount_id(String|ObjectId) - Account ID_id(String|ObjectId, optional) - Document ID
Returns: Promise<Object|false> - Key document or false if not found
Example Usage:
const key = await keysModel.findApiKey({
account_id: accountId,
_id: tokenId,
});
if (key) {
// Use key.api_key for API calls
}
Side Effects:
- โน๏ธ Database Read: Queries callrail.key collection
deleteAll(accountId)โ
Purpose: Delete all API keys for account (hard delete)
Source: Models/keys.js
Parameters:
accountId(ObjectId) - Account ID
Returns: Promise<Object> - Delete result
Example Usage:
await keysModel.deleteAll(new mongoose.Types.ObjectId(accountId));
// All keys for account deleted
Side Effects:
- โ ๏ธ Database Delete: Removes documents from database
update(condition, updateData)โ
Purpose: Update multiple key documents (used for soft delete)
Source: Models/keys.js
Parameters:
condition(Object) - Query conditionsupdateData(Object) - Update operations
Returns: Promise<Object> - Update result
Example Usage:
// Soft delete
await keysModel.update(
{ account_id: new mongoose.Types.ObjectId(accountId) },
{ deleted: new Date() },
);
// Mark as invalidated
await keysModel.update(
{ account_id: new mongoose.Types.ObjectId(accountId) },
{ $set: { token_invalidated: true } },
);
Side Effects:
- โ ๏ธ Database Update: Modifies multiple documents
๐ Integration Pointsโ
Campaign Integrationโ
Key Preservation Logic:
- API keys preserved if active campaigns exist
- Prevents breaking campaign tracking mid-flight
- Only user config deleted when campaigns active
Campaign Check:
const existingCampaigns = await campaignModel.findOne({
account_id: accountId,
is_deleted: false,
integration: 'callrail',
});
if (existingCampaigns) {
// Only delete user config, keep API key
} else {
// Safe to delete both config and key
}
Notification Systemโ
Integration Points:
- FCM notifications for integration connect/disconnect
- Email notifications based on user preferences
- Parent account notifications for sub-accounts
Notification Types:
project_added- When integration addedintegration_disconnected- When integration removed
๐งช Edge Cases & Special Handlingโ
Invalidated Keysโ
Issue: CallRail API returns 401 when key is invalid
Handling:
- Main error handler in
index.jscatches 401 errors - Sets
token_invalidated: trueon all account keys - User must re-authenticate with new key
Soft Delete vs Hard Deleteโ
Issue: Need to preserve keys for active campaigns
Handling:
- Soft Delete: Set
deletedtimestamp, key remains in database - Hard Delete:
deleteAll()removes documents completely - Soft delete used by default to preserve historical data
Multiple Keys Per Accountโ
Issue: System designed for one key per account
Handling:
findApiKeyusesfindOne()- returns first matchdeleteAllremoves all keys when invalidated- UI should prevent creating multiple valid keys
Sub-Account Notificationsโ
Issue: Sub-accounts may need to notify parent account
Handling:
- Check if
parent_accountexists and differs fromaccount_id - Send duplicate notifications to parent account
- Notifications indicate "for projects" context
โ ๏ธ Important Notesโ
- ๐ API Key Format: 32-character hexadecimal string from CallRail dashboard
- ๐๏ธ Soft Delete: Keys marked as deleted, not removed from database
- ๐ Campaign Preservation: Keys kept if active campaigns exist
- ๐ Token Invalidation: Automatic detection on 401 errors
- ๐ Notifications: Email and FCM notifications on connect/disconnect
- ๐ฅ Parent Account: Sub-accounts send notifications to parent
- ๐ User Config: Separate document links CallRail to DashClicks campaigns
- โ ๏ธ Global Delete: Use
global=truequery param for complete removal
๐ Related Documentationโ
- Integration Overview: CallRail Integration
- Accounts & Companies: ./accounts-companies.md
- CallRail API: CallRail API Documentation