Skip to content

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