Appearance
JavaScript SDK Integration
This example demonstrates how to integrate with the Engagifii API using JavaScript.
Code Example
javascript
/**
* Engagifii CRM API JavaScript SDK
*
* A comprehensive JavaScript SDK for integrating with the Engagifii CRM API.
* This SDK provides easy-to-use methods for authentication, people management,
* organization management, and more.
*
* @version 1.0.0
* @author Engagifii API Team
*/
class EngagifiiCRM {
/**
* Initialize the Engagifii CRM SDK
* @param {Object} config - Configuration object
* @param {string} config.clientId - OAuth2 client ID
* @param {string} config.clientSecret - OAuth2 client secret
* @param {string} config.tenantCode - Your organization's tenant code
* @param {string} config.baseURL - API base URL (optional, defaults to production)
*/
constructor(config) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.tenantCode = config.tenantCode;
this.baseURL = config.baseURL || 'https://builtin-crm.azurewebsites.net/api/v1';
this.tokenURL = config.tokenURL || 'https://builtin-crm.azurewebsites.net/oauth/token';
this.accessToken = null;
this.tokenExpiry = null;
this.requestCount = 0;
this.rateLimitRemaining = 1000;
}
/**
* Authenticate and obtain access token
* @returns {Promise<string>} Access token
*/
async authenticate() {
try {
const response = await fetch(this.tokenURL, {
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
})
});
if (!response.ok) {
const error = await response.json();
throw new EngagifiiAPIError(
`Authentication failed: ${error.error_description || error.error}`,
response.status,
'AUTH_ERROR'
);
}
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.tokenExpiry = Date.now() + (tokenData.expires_in * 1000);
return this.accessToken;
} catch (error) {
if (error instanceof EngagifiiAPIError) {
throw error;
}
throw new EngagifiiAPIError(`Authentication failed: ${error.message}`, 500, 'NETWORK_ERROR');
}
}
/**
* Check if token is valid and refresh if needed
* @returns {Promise<string>} Valid access token
*/
async getValidToken() {
if (!this.accessToken || Date.now() >= (this.tokenExpiry - 300000)) { // Refresh 5 mins before expiry
await this.authenticate();
}
return this.accessToken;
}
/**
* Make authenticated API request with automatic retry and error handling
* @param {string} endpoint - API endpoint path
* @param {Object} options - Request options
* @returns {Promise<Object>} API response data
*/
async request(endpoint, options = {}) {
const token = await this.getValidToken();
const url = `${this.baseURL}${endpoint}`;
const defaultHeaders = {
'Authorization': `Bearer ${token}`,
'tenant-code': this.tenantCode,
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const requestOptions = {
...options,
headers: {
...defaultHeaders,
...options.headers
}
};
return this._makeRequestWithRetry(url, requestOptions);
}
/**
* Internal method to handle requests with retry logic
* @private
*/
async _makeRequestWithRetry(url, options, attempt = 1, maxRetries = 3) {
try {
const response = await fetch(url, options);
// Update rate limit tracking
this._updateRateLimit(response.headers);
if (response.ok) {
const data = await response.json();
return data;
}
// Handle specific error cases
if (response.status === 429) {
return this._handleRateLimit(response, url, options, attempt, maxRetries);
}
if (response.status === 401 && attempt === 1) {
// Try to refresh token and retry once
await this.authenticate();
const newOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${this.accessToken}`
}
};
return this._makeRequestWithRetry(url, newOptions, attempt + 1, maxRetries);
}
// Parse error response
const errorData = await response.json();
throw new EngagifiiAPIError(
errorData.error?.message || `HTTP ${response.status}`,
response.status,
errorData.error?.code || 'API_ERROR',
errorData.error?.details,
errorData.error?.requestId
);
} catch (error) {
if (error instanceof EngagifiiAPIError) {
throw error;
}
// Retry on network errors
if (attempt < maxRetries && this._isRetryableError(error)) {
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Exponential backoff
await this._sleep(delay + Math.random() * 1000); // Add jitter
return this._makeRequestWithRetry(url, options, attempt + 1, maxRetries);
}
throw new EngagifiiAPIError(`Network error: ${error.message}`, 0, 'NETWORK_ERROR');
}
}
/**
* Handle rate limiting with backoff
* @private
*/
async _handleRateLimit(response, url, options, attempt, maxRetries) {
if (attempt >= maxRetries) {
throw new EngagifiiAPIError('Rate limit exceeded', 429, 'RATE_LIMIT_EXCEEDED');
}
const retryAfter = response.headers.get('Retry-After');
const resetTime = response.headers.get('X-RateLimit-Reset');
let waitTime = 60000; // Default 1 minute
if (retryAfter) {
waitTime = parseInt(retryAfter) * 1000;
} else if (resetTime) {
waitTime = Math.max((parseInt(resetTime) * 1000) - Date.now(), 0);
}
console.warn(`Rate limited. Waiting ${waitTime}ms before retry (attempt ${attempt})`);
await this._sleep(waitTime);
return this._makeRequestWithRetry(url, options, attempt + 1, maxRetries);
}
/**
* Update rate limit tracking from response headers
* @private
*/
_updateRateLimit(headers) {
const remaining = headers.get('X-RateLimit-Remaining');
if (remaining !== null) {
this.rateLimitRemaining = parseInt(remaining);
}
}
/**
* Check if error is retryable
* @private
*/
_isRetryableError(error) {
return error.code === 'ECONNRESET' ||
error.code === 'ECONNREFUSED' ||
error.code === 'ETIMEDOUT' ||
error.name === 'FetchError';
}
/**
* Sleep utility
* @private
*/
_sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ==================== PEOPLE MANAGEMENT ====================
/**
* List people with pagination and filtering
* @param {Object} options - Query options
* @returns {Promise<Object>} Paginated people list
*/
async listPeople(options = {}) {
const requestBody = {
pageIndex: options.pageIndex || 0,
pageSize: options.pageSize || 50,
sortField: options.sortField || 'lastName',
sortDirection: options.sortDirection || 'asc',
searchText: options.searchText,
filter: options.filter || {}
};
return this.request('/people/PeoplePagingList', {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
/**
* Get person by ID
* @param {string} personId - Person GUID
* @returns {Promise<Object>} Person details
*/
async getPerson(personId) {
if (!personId) {
throw new EngagifiiAPIError('Person ID is required', 400, 'INVALID_PARAMETER');
}
return this.request(`/people/${personId}`);
}
/**
* Create a new person
* @param {string} organizationId - Organization GUID to associate person with
* @param {Object} personData - Person information
* @returns {Promise<Object>} Created person
*/
async createPerson(organizationId, personData) {
if (!organizationId || !personData) {
throw new EngagifiiAPIError('Organization ID and person data are required', 400, 'INVALID_PARAMETER');
}
// Validate required fields
if (!personData.firstName || !personData.lastName || !personData.email) {
throw new EngagifiiAPIError('firstName, lastName, and email are required', 400, 'MISSING_REQUIRED_FIELDS');
}
return this.request(`/people/CreatePersonInline/${organizationId}`, {
method: 'POST',
body: JSON.stringify(personData)
});
}
/**
* Update person information
* @param {string} personId - Person GUID
* @param {Object} updateData - Fields to update
* @returns {Promise<Object>} Updated person
*/
async updatePerson(personId, updateData) {
if (!personId || !updateData) {
throw new EngagifiiAPIError('Person ID and update data are required', 400, 'INVALID_PARAMETER');
}
return this.request(`/people/UpdatePersonHeader/${personId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
}
/**
* Search people by text
* @param {string} searchText - Search query
* @param {Object} options - Additional search options
* @returns {Promise<Object>} Search results
*/
async searchPeople(searchText, options = {}) {
if (!searchText) {
throw new EngagifiiAPIError('Search text is required', 400, 'INVALID_PARAMETER');
}
const requestBody = {
pageIndex: options.pageIndex || 0,
pageSize: options.pageSize || 50,
filter: options.filter || {}
};
return this.request(`/people/SearchPeopleList/${encodeURIComponent(searchText)}`, {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
/**
* Add tags to multiple people
* @param {string[]} peopleIds - Array of person GUIDs
* @param {string[]} tagIds - Array of tag IDs
* @returns {Promise<Object>} Operation result
*/
async addTagsToPeople(peopleIds, tagIds) {
if (!Array.isArray(peopleIds) || !Array.isArray(tagIds)) {
throw new EngagifiiAPIError('peopleIds and tagIds must be arrays', 400, 'INVALID_PARAMETER');
}
return this.request('/people/AddPeoplesTags', {
method: 'POST',
body: JSON.stringify({
peopleIds,
tagIds,
action: 'add'
})
});
}
// ==================== ORGANIZATION MANAGEMENT ====================
/**
* List organizations with pagination and filtering
* @param {Object} options - Query options
* @returns {Promise<Object>} Paginated organizations list
*/
async listOrganizations(options = {}) {
const requestBody = {
pageIndex: options.pageIndex || 0,
pageSize: options.pageSize || 50,
sortField: options.sortField || 'name',
sortDirection: options.sortDirection || 'asc',
filter: options.filter || {}
};
return this.request('/organization/OrganizationPagingList', {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
/**
* Get organization by ID
* @param {string} organizationId - Organization GUID
* @returns {Promise<Object>} Organization details
*/
async getOrganization(organizationId) {
if (!organizationId) {
throw new EngagifiiAPIError('Organization ID is required', 400, 'INVALID_PARAMETER');
}
return this.request(`/organization/${organizationId}`);
}
/**
* Create a new organization
* @param {Object} organizationData - Organization information
* @returns {Promise<Object>} Created organization
*/
async createOrganization(organizationData) {
if (!organizationData || !organizationData.name || !organizationData.type) {
throw new EngagifiiAPIError('Organization name and type are required', 400, 'MISSING_REQUIRED_FIELDS');
}
return this.request('/organization', {
method: 'POST',
body: JSON.stringify(organizationData)
});
}
/**
* Get organization members
* @param {string} organizationId - Organization GUID
* @param {Object} options - Query options
* @returns {Promise<Object>} Organization members
*/
async getOrganizationMembers(organizationId, options = {}) {
if (!organizationId) {
throw new EngagifiiAPIError('Organization ID is required', 400, 'INVALID_PARAMETER');
}
const requestBody = {
pageIndex: options.pageIndex || 0,
pageSize: options.pageSize || 50,
filter: options.filter || {}
};
return this.request(`/organization/${organizationId}/GetAllPeople`, {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
// ==================== COMMITTEE MANAGEMENT ====================
/**
* List committees with pagination and filtering
* @param {Object} options - Query options
* @returns {Promise<Object>} Paginated committees list
*/
async listCommittees(options = {}) {
const requestBody = {
pageIndex: options.pageIndex || 0,
pageSize: options.pageSize || 25,
sortField: options.sortField || 'name',
sortDirection: options.sortDirection || 'asc',
filter: options.filter || {}
};
return this.request('/committee/CommiteePagingList', {
method: 'POST',
body: JSON.stringify(requestBody)
});
}
/**
* Get committee by ID
* @param {string} committeeId - Committee ID
* @returns {Promise<Object>} Committee details
*/
async getCommittee(committeeId) {
if (!committeeId) {
throw new EngagifiiAPIError('Committee ID is required', 400, 'INVALID_PARAMETER');
}
return this.request(`/committee/get/${committeeId}`);
}
/**
* Create a new committee
* @param {Object} committeeData - Committee information
* @returns {Promise<Object>} Created committee
*/
async createCommittee(committeeData) {
if (!committeeData || !committeeData.name || !committeeData.organizationId) {
throw new EngagifiiAPIError('Committee name and organization ID are required', 400, 'MISSING_REQUIRED_FIELDS');
}
return this.request('/committee/createcommittee', {
method: 'POST',
body: JSON.stringify(committeeData)
});
}
// ==================== ADVOCACY & RELATIONSHIPS ====================
/**
* Get relationship types
* @returns {Promise<Object[]>} Available relationship types
*/
async getRelationshipTypes() {
return this.request('/advocacy/GetRelationshipTypes');
}
/**
* Get people relationships
* @param {string} personId - Person GUID
* @returns {Promise<Object[]>} Person's relationships
*/
async getPeopleRelationships(personId) {
if (!personId) {
throw new EngagifiiAPIError('Person ID is required', 400, 'INVALID_PARAMETER');
}
return this.request(`/advocacy/list/people-relationships/${personId}`);
}
/**
* Save people relationship
* @param {Object} relationshipData - Relationship information
* @returns {Promise<Object>} Created/updated relationship
*/
async savePeopleRelationship(relationshipData) {
if (!relationshipData || !relationshipData.personId || !relationshipData.relatedOfficialId) {
throw new EngagifiiAPIError('Person ID and related official ID are required', 400, 'MISSING_REQUIRED_FIELDS');
}
return this.request('/advocacy/save/people-relationship', {
method: 'POST',
body: JSON.stringify(relationshipData)
});
}
// ==================== BATCH OPERATIONS ====================
/**
* Process multiple people in batch
* @param {Object[]} operations - Array of operations to perform
* @returns {Promise<Object[]>} Results of batch operations
*/
async batchProcessPeople(operations) {
if (!Array.isArray(operations) || operations.length === 0) {
throw new EngagifiiAPIError('Operations array is required and cannot be empty', 400, 'INVALID_PARAMETER');
}
const results = [];
const batchSize = 10; // Process in batches to avoid rate limits
for (let i = 0; i < operations.length; i += batchSize) {
const batch = operations.slice(i, i + batchSize);
const batchPromises = batch.map(async (operation) => {
try {
switch (operation.type) {
case 'create':
return await this.createPerson(operation.organizationId, operation.data);
case 'update':
return await this.updatePerson(operation.personId, operation.data);
case 'get':
return await this.getPerson(operation.personId);
default:
throw new EngagifiiAPIError(`Unknown operation type: ${operation.type}`, 400, 'INVALID_OPERATION');
}
} catch (error) {
return { error: error.message, operation };
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
// Add small delay between batches to be respectful of rate limits
if (i + batchSize < operations.length) {
await this._sleep(100);
}
}
return results;
}
// ==================== UTILITY METHODS ====================
/**
* Get current rate limit status
* @returns {Object} Rate limit information
*/
getRateLimitStatus() {
return {
remaining: this.rateLimitRemaining,
requestCount: this.requestCount,
hasActiveToken: !!this.accessToken,
tokenExpiry: this.tokenExpiry ? new Date(this.tokenExpiry) : null
};
}
/**
* Health check - verify API connectivity and authentication
* @returns {Promise<Object>} Health status
*/
async healthCheck() {
try {
// Try to get relationship types as a simple authenticated endpoint
await this.getRelationshipTypes();
return {
status: 'healthy',
authenticated: true,
timestamp: new Date().toISOString(),
rateLimitRemaining: this.rateLimitRemaining
};
} catch (error) {
return {
status: 'unhealthy',
authenticated: false,
error: error.message,
timestamp: new Date().toISOString()
};
}
}
}
/**
* Custom error class for Engagifii API errors
*/
class EngagifiiAPIError extends Error {
constructor(message, status = 0, code = 'UNKNOWN_ERROR', details = null, requestId = null) {
super(message);
this.name = 'EngagifiiAPIError';
this.status = status;
this.code = code;
this.details = details;
this.requestId = requestId;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
code: this.code,
details: this.details,
requestId: this.requestId,
timestamp: this.timestamp
};
}
}
// ==================== USAGE EXAMPLES ====================
/**
* Example usage of the Engagifii CRM SDK
*/
async function exampleUsage() {
// Initialize the SDK
const crm = new EngagifiiCRM({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
tenantCode: 'your-tenant-code'
});
try {
// Health check
const health = await crm.healthCheck();
console.log('API Health:', health);
// List people with filtering
const people = await crm.listPeople({
pageSize: 25,
filter: {
isActive: true,
organizationId: 'some-org-id'
}
});
console.log(`Found ${people.totalCount} people`);
// Create a new organization
const newOrg = await crm.createOrganization({
name: 'Example Corporation',
type: 'Company',
email: 'info@example.com',
website: 'https://example.com'
});
console.log('Created organization:', newOrg.id);
// Create a new person
const newPerson = await crm.createPerson(newOrg.id, {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane.smith@example.com',
phone: '+1-555-987-6543',
title: 'Marketing Director'
});
console.log('Created person:', newPerson.id);
// Search for people
const searchResults = await crm.searchPeople('jane smith', {
pageSize: 10,
filter: { isActive: true }
});
console.log('Search results:', searchResults.totalCount);
} catch (error) {
if (error instanceof EngagifiiAPIError) {
console.error('API Error:', error.toJSON());
} else {
console.error('Unexpected error:', error);
}
}
}
// Export for use in Node.js or browser environments
if (typeof module !== 'undefined' && module.exports) {
module.exports = { EngagifiiCRM, EngagifiiAPIError };
} else if (typeof window !== 'undefined') {
window.EngagifiiCRM = EngagifiiCRM;
window.EngagifiiAPIError = EngagifiiAPIError;
}Usage Notes
- Make sure to replace placeholder values with your actual API credentials
- Install required dependencies before running this code
- Refer to the main API documentation for detailed endpoint information
Download
Download this example: javascript-sdk.js
