Custom CRM Configuration - HaloPSA Workflow
Overview
This document defines the custom CRM workflow configuration for HaloPSA, including entity types, lifecycle stages, and specific attributes for B2B data integration.
Workflow Progression (Lifecycle)
Complete Workflow Overview
flowchart TD
subgraph LEAD_FLOW ["π― LEAD LIFECYCLE"]
L1(["New Lead"])
L2(["Researching"])
L3(["Contacted"])
L4(["Engaged"])
L5(["No Interest"])
L6(["Do Not Contact"])
L7(["Invalid Data"])
L1 -.->|"Claim for research"| L2
L2 -.->|"Establish contact"| L3
L3 -.->|"Interested"| L4
L3 -.->|"Not interested"| L5
L3 -.->|"Opt-out request"| L6
L2 -.->|"Bad data"| L7
end
subgraph PROSPECT_FLOW ["π PROSPECT LIFECYCLE"]
P1(["New Prospect"])
P2(["Prospecting"])
P3(["Qualified"])
P4(["Disqualified"])
P1 -.->|"Identify problem"| P2
P2 -.->|"Meets criteria"| P3
P2 -.->|"Doesn't qualify"| P4
end
subgraph OPPORTUNITY_FLOW ["π° OPPORTUNITY LIFECYCLE"]
O1(["New Opportunity"])
O2(["Progressing"])
O3(["Negotiation"])
O4(["Won"])
O5(["Lost"])
O1 -.->|"Propose solution"| O2
O2 -.->|"Discuss terms"| O3
O3 -.->|"Close deal"| O4
O3 -.->|"Deal lost"| O5
end
%% Promotion flows
L4 ==>|"Convert to Prospect"| P1
P3 ==>|"Promote to Opportunity"| O1
%% Styling
classDef leadNode fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
classDef prospectNode fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef opportunityNode fill:#e8f5e8,stroke:#388e3c,stroke-width:2px
classDef conversionNode fill:#fff3e0,stroke:#f57c00,stroke-width:3px
class L1,L2,L3,L4,L5,L6,L7 leadNode
class P1,P2,P3,P4 prospectNode
class O1,O2,O3,O4,O5 opportunityNode
class L4,P3 conversionNode
Detailed Stage Workflows
π― LEAD Lifecycle Detail
flowchart LR
subgraph RESEARCH ["π Research Stage"]
L1(["New Lead"])
L2(["Researching"])
L1 -.->|"Claim & start research"| L2
end
subgraph CONTACT ["π Introduction Stage"]
L3(["Contacted"])
L3
L2 -.->|"Establish contact"| L3
end
subgraph QUALIFY ["β
Conversion Stage"]
L4(["Engaged"])
L5(["No Interest"])
L6(["Do Not Contact"])
L7(["Invalid Data"])
L4
L5
L6
L7
L3 -.->|"Shows interest"| L4
L3 -.->|"Not interested"| L5
L3 -.->|"Requests removal"| L6
L2 -.->|"Data issues"| L7
end
%% Promotion
L4 ==>|"π Convert to Prospect"| P1(["New Prospect"])
%% Styling
classDef activeNode fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#fff
classDef inactiveNode fill:#f44336,stroke:#c62828,stroke-width:2px,color:#fff
classDef neutralNode fill:#ff9800,stroke:#ef6c00,stroke-width:2px,color:#fff
classDef processNode fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#fff
class L4 activeNode
class L5,L6,L7 inactiveNode
class P1 activeNode
class L1,L2,L3 processNode
π PROSPECT Lifecycle Detail
flowchart LR
subgraph PROSPECT_DETAIL ["π Prospect Qualification Process"]
P1(["New Prospect"])
P2(["Prospecting"])
P3(["Qualified"])
P4(["Disqualified"])
P1 -.->|"Identify pain points"| P2
P2 -.->|"β Budget + Timeline + Authority"| P3
P2 -.->|"β Missing key criteria"| P4
end
subgraph QUAL_CRITERIA ["π Qualification Criteria"]
QC1["β Confirmed Pain Points"]
QC2["β Budget Range Identified"]
QC3["β Decision Maker Access"]
QC4["β Implementation Timeframe"]
QC5["β Fit Score β₯ 70"]
end
P2 -.-> QC1
P2 -.-> QC2
P2 -.-> QC3
P2 -.-> QC4
P2 -.-> QC5
QC1 & QC2 & QC3 & QC4 & QC5 -.->|"All criteria met"| P3
%% Promotion
P3 ==>|"π Promote to Opportunity"| O1(["New Opportunity"])
%% Styling
classDef qualifiedNode fill:#4caf50,stroke:#2e7d32,stroke-width:2px,color:#fff
classDef disqualifiedNode fill:#f44336,stroke:#c62828,stroke-width:2px,color:#fff
classDef processNode fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#fff
classDef criteriaNode fill:#ff9800,stroke:#ef6c00,stroke-width:1px
class P3,O1 qualifiedNode
class P4 disqualifiedNode
class P1,P2 processNode
class QC1,QC2,QC3,QC4,QC5 criteriaNode
π° OPPORTUNITY Lifecycle Detail
flowchart LR
subgraph OPP_DETAIL ["π° Sales Process"]
O1(["New Opportunity"])
O2(["Progressing"])
O3(["Negotiation"])
O4(["Won"])
O5(["Lost"])
O1 -.->|"Solution presentation"| O2
O2 -.->|"Proposal & commercials"| O3
O3 -.->|"β Deal closed"| O4
O3 -.->|"β Deal lost"| O5
end
subgraph SALES_DATA ["π Sales Tracking"]
SD1["π΅ Opportunity Value"]
SD2["π Probability %"]
SD3["π
Expected Close Date"]
SD4["π Quotes/Proposals"]
SD5["π’ Competitors"]
SD6["π Win/Loss Reason"]
end
O1 -.-> SD1
O2 -.-> SD2
O2 -.-> SD3
O2 -.-> SD4
O3 -.-> SD5
O4 -.-> SD6
O5 -.-> SD6
%% Styling
classDef wonNode fill:#4caf50,stroke:#2e7d32,stroke-width:3px,color:#fff
classDef lostNode fill:#f44336,stroke:#c62828,stroke-width:2px,color:#fff
classDef processNode fill:#2196f3,stroke:#1565c0,stroke-width:2px,color:#fff
classDef dataNode fill:#9c27b0,stroke:#6a1b9a,stroke-width:1px,color:#fff
class O4 wonNode
class O5 lostNode
class O1,O2,O3 processNode
class SD1,SD2,SD3,SD4,SD5,SD6 dataNode
Entity Types & Attributes
Entity Relationship & Data Flow
flowchart TD
subgraph ENTITIES ["π CRM Entities & Data Inheritance"]
subgraph LEAD_ENT ["π― LEAD"]
L_CONTACT["π Contact Data<br/>β’ Person Name<br/>β’ Job Title<br/>β’ Email<br/>β’ Phone Numbers<br/>β’ Company Name<br/>β’ Website<br/>β’ Industry<br/>β’ Headcount<br/>β’ Lead Source<br/>β’ Do Not Contact"]
L_RESEARCH["π Research Data<br/>β’ Services Offered<br/>β’ Public Research<br/>β’ News & Articles<br/>β’ Growth Signals<br/>β’ Project Pipelines<br/>β’ Initial Notes"]
end
subgraph PROSPECT_ENT ["π PROSPECT"]
P_INHERITED["π Inherited from Lead<br/>All Contact + Research Data"]
P_QUAL["β
Qualification Data<br/>β’ Confirmed Pain Points<br/>β’ Qualified Services<br/>β’ Decision Maker<br/>β’ Budget Range<br/>β’ Timeframe<br/>β’ Fit Score<br/>β’ Qualification Notes"]
end
subgraph OPPORTUNITY_ENT ["π° OPPORTUNITY"]
O_INHERITED["π Inherited Data<br/>Contact + Research + Qualification"]
O_SALES["πΌ Sales Data<br/>β’ Opportunity Value<br/>β’ Products/Services<br/>β’ Probability %<br/>β’ Expected Close Date<br/>β’ Quotes/Proposals<br/>β’ Competitors<br/>β’ Win/Loss Reason"]
end
subgraph CALL_ENT ["π CALL RECORD"]
C_DATA["π² Call Data<br/>β’ Date & Time<br/>β’ Direction<br/>β’ Dialled Number<br/>β’ Duration<br/>β’ Agent<br/>β’ Outcome<br/>β’ Next Action<br/>β’ Notes"]
end
end
%% Data inheritance flows
L_CONTACT --> P_INHERITED
L_RESEARCH --> P_INHERITED
P_INHERITED --> O_INHERITED
P_QUAL --> O_INHERITED
%% Call relationships
LEAD_ENT -.-> CALL_ENT
PROSPECT_ENT -.-> CALL_ENT
OPPORTUNITY_ENT -.-> CALL_ENT
%% Styling
classDef leadStyle fill:#e1f5fe,stroke:#0277bd,stroke-width:2px
classDef prospectStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
classDef opportunityStyle fill:#e8f5e8,stroke:#388e3c,stroke-width:2px
classDef callStyle fill:#fff3e0,stroke:#f57c00,stroke-width:2px
classDef dataStyle fill:#fafafa,stroke:#616161,stroke-width:1px
class LEAD_ENT leadStyle
class PROSPECT_ENT prospectStyle
class OPPORTUNITY_ENT opportunityStyle
class CALL_ENT callStyle
class L_CONTACT,L_RESEARCH,P_INHERITED,P_QUAL,O_INHERITED,O_SALES,C_DATA dataStyle
Detailed Entity Specifications
π― LEAD Entity
π Contact Data
| Field Name | Type | Required | Description | |ββββ|ββ|βββ-|ββββ-| | Person Name | String | β Required | Full name of the contact person | | Job Title | String | Optional | Position/role within the company | | Email | String | β Required | Primary email address | | Phone Numbers | String | Optional | Contact phone number(s) | | Company Name | String | β Required | Organization name | | Website | String | Optional | Company website URL | | Industry | String | Optional | Industry classification | | Headcount | Integer | Optional | Number of employees | | Lead Source | String | Optional | Source of the lead (Apollo, ZoomInfo, etc.) | | Do Not Contact | Boolean | Optional | Opt-out flag for contact preferences |
π Research & Intelligence
| Field Name | Type | Required | Description | |ββββ|ββ|βββ-|ββββ-| | Services Offered | Text | Optional | Companyβs primary services/products | | Public Research | Text | Optional | General company research findings | | News & Articles | Text | Optional | Recent news or press mentions | | Growth Signals | Text | Optional | Indicators of company growth/expansion | | Project Pipelines | Text | Optional | Known upcoming projects or initiatives | | Initial Notes | Text | Optional | General notes and observations |
π·οΈ Custom Fields (CF_101-113)
| Field ID | Field Name | Type | HaloPSA Mapping | Description | |βββ-|ββββ|ββ|ββββββ|ββββ-| | CF_101 | Lead Source | Dropdown | Custom Field 101 | Source platform identification | | CF_102 | Services Offered | Text | Custom Field 102 | Primary business offerings | | CF_103 | Growth Signals | Text | Custom Field 103 | Expansion/growth indicators | | CF_104 | Project Pipelines | Text | Custom Field 104 | Pipeline opportunities | | CF_105 | Do Not Contact | Boolean | Custom Field 105 | Contact restriction flag | | CF_106 | Technology Stack | Text | Custom Field 106 | Current technologies and software in use | | CF_107 | Revenue Range | Dropdown | Custom Field 107 | Annual revenue bracket ($1M-5M, $5M-25M, etc.) | | CF_108 | Employee Count Range | Dropdown | Custom Field 108 | Company size category (10-50, 51-200, 201-1000, etc.) | | CF_109 | Management Level | Dropdown | Custom Field 109 | Contactβs seniority (C-Suite, VP, Director, Manager, etc.) | | CF_110 | Department Function | Dropdown | Custom Field 110 | Contactβs department (IT, Finance, Operations, etc.) | | CF_111 | Intent Signals | Text | Custom Field 111 | Buying intent indicators and behavioral signals | | CF_112 | Company Founded Year | Integer | Custom Field 112 | Year company was established | | CF_113 | Location/HQ Address | Text | Custom Field 113 | Company headquarters location |
π PROSPECT Entity
π Inherited from Lead
| Field Category | Source | Description | |βββββ-|βββ|ββββ-| | Contact Data | LEAD Entity | All contact information fields (Name, Email, Phone, Company, etc.) | | Research Data | LEAD Entity | All research and intelligence fields (Services, Growth Signals, etc.) |
β Qualification Data
| Field Name | Type | Required | Description | |ββββ|ββ|βββ-|ββββ-| | Confirmed Pain Points | Text | β Required | Identified business challenges/needs | | Qualified Services | Text | β Required | Services that match prospectβs needs | | Decision Maker | String | β Required | Primary decision maker identification | | Budget Range | String | β Required | Estimated budget availability | | Timeframe | String | β Required | Expected implementation timeline | | Fit Score (0-100) | Integer | β Required | Qualification score out of 100 | | Qualification Notes | Text | Optional | Additional qualification observations |
π·οΈ Custom Fields (CF_201-206)
| Field ID | Field Name | Type | HaloPSA Mapping | Description | |βββ-|ββββ|ββ|ββββββ|ββββ-| | CF_201 | Pain Points | Text | Custom Field 201 | Confirmed business challenges | | CF_202 | Qualified Services | Text | Custom Field 202 | Matching service offerings | | CF_203 | Decision Maker | String | Custom Field 203 | Key stakeholder information | | CF_204 | Budget Range | String | Custom Field 204 | Available budget estimation | | CF_205 | Timeframe | String | Custom Field 205 | Implementation schedule | | CF_206 | Fit Score | Integer | Custom Field 206 | Overall qualification score |
π° OPPORTUNITY Entity
π Inherited Data
| Field Category | Source | Description | |βββββ-|βββ|ββββ-| | Contact Data | LEAD Entity | All contact information fields | | Research Data | LEAD Entity | All research and intelligence fields | | Qualification Data | PROSPECT Entity | All qualification and fit scoring data |
πΌ Sales Data
| Field Name | Type | Required | Description | |ββββ|ββ|βββ-|ββββ-| | Opportunity Value | Currency | β Required | Total opportunity dollar amount | | Products/Services | Text | β Required | Specific products/services being sold | | Probability % | Integer | β Required | Likelihood of closing (0-100%) | | Expected Close Date | Date | β Required | Target deal closure date | | Quotes/Proposals | Text | Optional | Quotes and proposal documentation | | Competitors | Text | Optional | Competing vendors/solutions | | Win/Loss Reason | Text | Optional | Outcome explanation when closed |
π·οΈ Custom Fields (CF_301-304)
| Field ID | Field Name | Type | HaloPSA Mapping | Description | |βββ-|ββββ|ββ|ββββββ|ββββ-| | CF_301 | Products/Services | Text | Custom Field 301 | Solution components being offered | | CF_302 | Quotes/Proposals | Text | Custom Field 302 | Proposal and pricing information | | CF_303 | Competitors | Text | Custom Field 303 | Competitive landscape details | | CF_304 | Win/Loss Reason | Text | Custom Field 304 | Deal outcome analysis |
Status Values
Lead Status Options
new_lead- New Leadresearching- Researchingcontacted- Contactedengaged- Engagedno_interest- No Interestdo_not_contact- Do Not Contactinvalid_data- Invalid Data
Prospect Status Options
new_prospect- New Prospectprospecting- Prospectingqualified- Qualifieddisqualified- Disqualified
Opportunity Status Options
new_opportunity- New Opportunityprogressing- Progressingnegotiation- Negotiationwon- Wonlost- Lost
Custom Field Mappings
Lead Custom Fields
- Lead Source (CF_101) - Source system (Apollo.io, ZoomInfo, etc.)
- Services Offered (CF_102) - Company service offerings
- Growth Signals (CF_103) - Growth indicators
- Project Pipelines (CF_104) - Known projects
- Do Not Contact Flag (CF_105) - Boolean opt-out flag
Prospect Custom Fields
- Pain Points (CF_201) - Confirmed business challenges
- Qualified Services (CF_202) - Matching services
- Decision Maker (CF_203) - Key contact information
- Budget Range (CF_204) - Available budget
- Timeframe (CF_205) - Implementation timeline
- Fit Score (CF_206) - Numerical fit rating
Opportunity Custom Fields
- Products Services (CF_301) - Proposed offerings
- Quotes Proposals (CF_302) - Associated quotes
- Competitors (CF_303) - Competing vendors
- Win Loss Reason (CF_304) - Outcome reason
Integration Considerations
Data Enrichment Mapping
Critical field mappings from B2B sources to HaloPSA Lead ticket fields and customFields. These mappings are essential for automation, deduplication, and workflow progression.
ZoomInfo API β HaloPSA Lead Mapping
Standard Ticket Fields
{
"summary": "firstName + ' ' + lastName + ' - ' + companyName",
"details": "Generated lead from ZoomInfo enrichment",
"user_name": "firstName + ' ' + lastName",
"reportedby": "email",
"category_1": "Lead",
"category_2": "ZoomInfo",
"status_id": 1,
"priority_id": 3
}
Contact Fields
{
"firstname": "firstName",
"surname": "lastName",
"emailaddress": "email",
"jobtitle": "jobTitle",
"phonenumber": "directPhone || companyPhone",
"linkedin_url": "linkedInUrl"
}
Company Fields
{
"name": "companyName",
"website": "companyWebsite",
"phonenumber": "companyPhone",
"notes": "Industry: | Employees: | Revenue: | Department: | Management Level: "
}
Custom Fields
{
"CF_101_lead_source": "ZoomInfo",
"CF_102_services_offered": "industry + ' | ' + companyDescription",
"CF_103_growth_signals": "revenue + ' | ' + employeeCount + ' employees | ' + companyGrowthRate",
"CF_104_project_pipelines": "technologies.join(', ')",
"CF_105_do_not_contact": false
}
Apollo.io API β HaloPSA Lead Mapping
Standard Ticket Fields
{
"summary": "person.first_name + ' ' + person.last_name + ' - ' + person.organization.name",
"details": "Generated lead from Apollo.io enrichment",
"user_name": "person.first_name + ' ' + person.last_name",
"reportedby": "person.email",
"category_1": "Lead",
"category_2": "Apollo.io",
"status": 1,
"priority": 3
}
Contact Fields Mapping
{
"person.first_name": "β HaloPSA User.firstname",
"person.last_name": "β HaloPSA User.surname",
"person.email": "β HaloPSA User.emailaddress",
"person.title": "β HaloPSA User.jobtitle",
"person.linkedin_url": "β HaloPSA User.linkedin_url",
"person.organization.name": "β HaloPSA Client.clientname",
"person.organization.website_url": "β HaloPSA Client.website",
"person.organization.phone": "β HaloPSA Client.phonenumber"
}
Custom Fields Mapping (CF_101-113)
{
"CF_101_lead_source": {
"id": 101,
"value": "Apollo.io",
"source_field": "Static value"
},
"CF_102_services_offered": {
"id": 102,
"value": "person.organization.keywords[]",
"transformation": "JOIN(', ')",
"fallback": "person.organization.industry"
},
"CF_103_growth_signals": {
"id": 103,
"value": "person.organization.funding_events[] || person.organization.technologies[]",
"transformation": "CONCAT funding + tech changes"
},
"CF_104_project_pipelines": {
"id": 104,
"value": "person.organization.current_technologies[]",
"transformation": "Filter tech stack for MSP relevance"
},
"CF_105_do_not_contact": {
"id": 105,
"value": "false",
"source_field": "Default value - check against suppression lists"
},
"CF_106_technology_stack": {
"id": 106,
"value": "person.organization.technologies[]",
"transformation": "JOIN tech stack array with separators"
},
"CF_107_revenue_range": {
"id": 107,
"value": "person.organization.estimated_num_employees",
"transformation": "Map to revenue brackets based on employee count"
},
"CF_108_employee_count_range": {
"id": 108,
"value": "person.organization.estimated_num_employees",
"transformation": "Categorize into size ranges"
},
"CF_109_management_level": {
"id": 109,
"value": "person.seniority",
"source_field": "person.title analysis for seniority level"
},
"CF_110_department_function": {
"id": 110,
"value": "person.departments[]",
"transformation": "Primary department identification"
},
"CF_111_intent_signals": {
"id": 111,
"value": "person.organization.intent_strength + person.organization.buyer_intent_topics[]",
"transformation": "Combine intent score with topics"
},
"CF_112_company_founded_year": {
"id": 112,
"value": "person.organization.founded_year",
"source_field": "Direct mapping"
},
"CF_113_location_hq": {
"id": 113,
"value": "person.organization.primary_domain + person.organization.headquarters_address",
"transformation": "Format complete address string"
}
}
Phone Number Processing
{
"person.phone_numbers[0].sanitized_number": "β HaloPSA User.phonenumber",
"person.phone_numbers[0].type": "β Custom note",
"fallback_processing": {
"person.organization.phone": "β Use if personal phone unavailable",
"format": "Clean and standardize to E.164 format"
}
}
ZoomInfo API β HaloPSA Lead Mapping
Standard Ticket Fields
{
"summary": "contact.firstName + ' ' + contact.lastName + ' - ' + company.companyName",
"details": "Generated lead from ZoomInfo enrichment",
"user_name": "contact.firstName + ' ' + contact.lastName",
"reportedby": "contact.email",
"category_1": "Lead",
"category_2": "ZoomInfo"
}
Contact & Company Mapping
{
"contact.firstName": "β HaloPSA User.firstname",
"contact.lastName": "β HaloPSA User.surname",
"contact.email": "β HaloPSA User.emailaddress",
"contact.jobTitle": "β HaloPSA User.jobtitle",
"contact.directPhoneNumber": "β HaloPSA User.phonenumber",
"contact.linkedInUrl": "β HaloPSA User.linkedin_url",
"company.companyName": "β HaloPSA Client.clientname",
"company.website": "β HaloPSA Client.website",
"company.phone": "β HaloPSA Client.phonenumber",
"company.industry": "β HaloPSA Client.notes (append)",
"company.revenue": "β CF_103 Growth Signals",
"company.employees": "β CF_103 Growth Signals"
}
ZoomInfo Custom Fields (CF_101-113)
{
"CF_101_lead_source": "ZoomInfo",
"CF_102_services_offered": "company.industry + company.keywordTags[]",
"CF_103_growth_signals": "company.revenue + ' | ' + company.employees + ' employees'",
"CF_104_project_pipelines": "company.technologiesUsed[]",
"CF_105_do_not_contact": "contact.doNotCall || contact.doNotEmail",
"CF_106_technology_stack": "company.technologiesUsed[] + company.webTechnologies[]",
"CF_107_revenue_range": "company.revenue",
"CF_108_employee_count_range": "company.employees",
"CF_109_management_level": "contact.managementLevel",
"CF_110_department_function": "contact.department",
"CF_111_intent_signals": "company.buyingIntentTopics[] + contact.recentJobChange",
"CF_112_company_founded_year": "company.foundedYear",
"CF_113_location_hq": "company.street + ', ' + company.city + ', ' + company.state + ' ' + company.zipCode"
}
Hunter.io API β HaloPSA Lead Mapping
Email Verification Focus
{
"domain_search.emails[].value": "β HaloPSA User.emailaddress",
"domain_search.emails[].first_name": "β HaloPSA User.firstname",
"domain_search.emails[].last_name": "β HaloPSA User.surname",
"domain_search.emails[].position": "β HaloPSA User.jobtitle",
"domain_search.emails[].confidence": "β CF_103 (data quality score)",
"domain_search.organization": "β HaloPSA Client.clientname",
"domain_search.domain": "β HaloPSA Client.website"
}
Hunter.io Extended Custom Fields (CF_101-113)
{
"CF_101_lead_source": "Hunter.io",
"CF_102_services_offered": "domain_search.pattern + domain_search.organization",
"CF_103_growth_signals": "domain_search.emails[].confidence + ' | Email deliverability: ' + domain_search.webmail",
"CF_104_project_pipelines": "domain_search.emails[].sources[]",
"CF_105_do_not_contact": "false",
"CF_106_technology_stack": "Not available via Hunter.io",
"CF_107_revenue_range": "Not available via Hunter.io",
"CF_108_employee_count_range": "domain_search.emails[].length (estimate)",
"CF_109_management_level": "Derived from domain_search.emails[].position",
"CF_110_department_function": "Derived from domain_search.emails[].position",
"CF_111_intent_signals": "domain_search.emails[].last_seen + verification status",
"CF_112_company_founded_year": "Not available via Hunter.io",
"CF_113_location_hq": "Not available via Hunter.io"
}
8 Additional High-Value Data Fields Summary
The following 8 additional data fields (CF_106-113) provide significant value for B2B lead qualification and sales intelligence across Apollo.io, ZoomInfo, and Hunter.io:
1. Technology Stack (CF_106) π₯οΈ
- Apollo.io:
person.organization.technologies[] - ZoomInfo:
company.technologiesUsed[] + company.webTechnologies[] - Hunter.io: Limited availability
- Value: Identifies MSP opportunities, technology refresh needs, and competitive intelligence
2. Revenue Range (CF_107) π°
- Apollo.io: Derived from
person.organization.estimated_num_employees - ZoomInfo:
company.revenue - Hunter.io: Not available
- Value: Budget qualification and deal sizing for proposal preparation
3. Employee Count Range (CF_108) π₯
- Apollo.io:
person.organization.estimated_num_employees - ZoomInfo:
company.employees - Hunter.io: Estimated from email count
- Value: Market segmentation and service package positioning
4. Management Level (CF_109) π―
- Apollo.io:
person.seniority - ZoomInfo:
contact.managementLevel - Hunter.io: Derived from job title analysis
- Value: Decision maker identification and sales approach customization
5. Department Function (CF_110) π’
- Apollo.io:
person.departments[] - ZoomInfo:
contact.department - Hunter.io: Derived from job title
- Value: Pain point targeting and solution alignment
6. Intent Signals (CF_111) π£
- Apollo.io:
person.organization.intent_strength + buyer_intent_topics[] - ZoomInfo:
company.buyingIntentTopics[] + contact.recentJobChange - Hunter.io: Email verification freshness
- Value: Lead scoring and sales timing optimization
7. Company Founded Year (CF_112) π
- Apollo.io:
person.organization.founded_year - ZoomInfo:
company.foundedYear - Hunter.io: Not available
- Value: Company maturity assessment and technology lifecycle timing
8. Location/HQ Address (CF_113) π
- Apollo.io:
person.organization.headquarters_address - ZoomInfo: Full address components
- Hunter.io: Not available
- Value: Regional sales territory assignment and compliance requirements
These additional fields significantly enhance lead qualification, sales intelligence, and CRM automation capabilities while maintaining compatibility across the three primary B2B data providers.
Data Transformation Rules
Data Quality Scoring
function calculateDataQualityScore(source, data) {
let score = 0;
// Email validation (30 points)
if (data.email && validateEmail(data.email)) score += 30;
// Phone validation (25 points)
if (data.phone && validatePhone(data.phone)) score += 25;
// Complete name (20 points)
if (data.firstName && data.lastName) score += 20;
// Company data (15 points)
if (data.companyName && data.website) score += 15;
// Job title (10 points)
if (data.jobTitle) score += 10;
return Math.min(score, 100);
}
Data Cleansing Rules
const dataCleansingRules = {
email: {
validation: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
normalize: email => email.toLowerCase().trim()
},
phone: {
clean: phone => phone.replace(/[^\d+]/g, ''),
format: phone => formatToE164(phone, defaultCountry)
},
companyName: {
clean: name => name.replace(/\b(Inc|LLC|Ltd|Corp)\b\.?/gi, '').trim(),
normalize: name => properCase(name)
},
website: {
normalize: url => url.replace(/^https?:\/\//, '').replace(/\/$/, '')
}
}
Multi-Source Data Precedence
When multiple sources provide the same data, use this precedence order:
const dataPrecedence = {
contactInfo: ['ZoomInfo', 'Apollo.io', 'Hunter.io'],
companyInfo: ['ZoomInfo', 'Apollo.io', 'Hunter.io'],
emailVerification: ['Hunter.io', 'ZoomInfo', 'Apollo.io'],
phoneNumbers: ['ZoomInfo', 'Apollo.io'],
socialMedia: ['Apollo.io', 'ZoomInfo'],
technographics: ['Apollo.io', 'ZoomInfo']
};
Deduplication Key Generation
function generateDeduplicationKey(leadData) {
const email = leadData.email ? leadData.email.toLowerCase() : '';
const company = leadData.companyName ? leadData.companyName.toLowerCase().replace(/[^a-z0-9]/g, '') : '';
const name = (leadData.firstName + leadData.lastName).toLowerCase().replace(/[^a-z]/g, '');
// Primary key: email
if (email) return `email:${email}`;
// Secondary key: company + name
if (company && name) return `company-person:${company}-${name}`;
// Fallback: phone number
if (leadData.phone) return `phone:${leadData.phone.replace(/[^\d]/g, '')}`;
return null; // Skip record if no deduplication key possible
}
Smart Update/Merge Strategy
When a lead already exists in HaloPSA (detected via deduplication key), use intelligent merge logic that preserves manual work while updating enriched data:
async function smartUpdateLead(existingTicket, newLeadData, haloApi) {
const updatePayload = {
id: existingTicket.id
};
const fieldsToUpdate = [];
const fieldsPreserved = [];
// NEVER update these workflow-critical fields if manually changed
const protectedFields = [
'status_id', // Preserve manual status changes
'agent_id', // Preserve assigned agent
'priority_id', // Preserve manual priority changes
'category_1', // Preserve lead classification
'notes', // Preserve manual notes/updates
'last_update_date' // System managed
];
// ALWAYS update these data enrichment fields (if new data available)
const enrichmentFields = {
// Contact data updates
'phonenumber': newLeadData.phone,
'linkedin_url': newLeadData.linkedinUrl,
// Custom field updates (CF_106-113 - new enrichment data)
'CF_106_technology_stack': newLeadData.technologyStack,
'CF_107_revenue_range': newLeadData.revenueRange,
'CF_108_employee_count_range': newLeadData.employeeCountRange,
'CF_109_management_level': newLeadData.managementLevel,
'CF_110_department_function': newLeadData.departmentFunction,
'CF_111_intent_signals': newLeadData.intentSignals,
'CF_112_company_founded_year': newLeadData.foundedYear,
'CF_113_location_hq': newLeadData.hqAddress
};
// CONDITIONALLY update these fields (only if empty or significantly different)
const conditionalFields = {
'CF_102_services_offered': {
existing: existingTicket.customfields?.find(f => f.id === 102)?.value,
new: newLeadData.servicesOffered,
updateIf: (existing, newVal) => !existing || (newVal && newVal.length > existing.length * 1.5)
},
'CF_103_growth_signals': {
existing: existingTicket.customfields?.find(f => f.id === 103)?.value,
new: newLeadData.growthSignals,
updateIf: (existing, newVal) => !existing || (newVal && !existing.includes(newVal))
},
'CF_104_project_pipelines': {
existing: existingTicket.customfields?.find(f => f.id === 104)?.value,
new: newLeadData.projectPipelines,
updateIf: (existing, newVal) => !existing || (newVal && !existing.includes(newVal))
}
};
// Process enrichment fields (always update if new data exists)
const customFieldUpdates = [];
Object.entries(enrichmentFields).forEach(([fieldKey, newValue]) => {
if (newValue && newValue !== '') {
const fieldId = parseInt(fieldKey.match(/CF_(\d+)/)?.[1]);
if (fieldId) {
customFieldUpdates.push({"id": fieldId, "value": newValue});
fieldsToUpdate.push(fieldKey);
}
}
});
// Process conditional fields
Object.entries(conditionalFields).forEach(([fieldKey, config]) => {
if (config.updateIf(config.existing, config.new)) {
const fieldId = parseInt(fieldKey.match(/CF_(\d+)/)?.[1]);
if (fieldId && config.new) {
customFieldUpdates.push({"id": fieldId, "value": config.new});
fieldsToUpdate.push(fieldKey);
}
} else {
fieldsPreserved.push(fieldKey);
}
});
// Check if status progression should be blocked
const hasManualUpdates = existingTicket.last_update_user !== 'B2B Integration';
const statusChanged = existingTicket.status_id !== 1; // Not "New Lead"
if (hasManualUpdates || statusChanged) {
fieldsPreserved.push('status_id', 'workflow_position');
console.log(`β οΈ Preserving manual changes for ticket ${existingTicket.id}`);
}
// Only proceed with update if there are fields to update
if (customFieldUpdates.length > 0) {
updatePayload.customfields = customFieldUpdates;
// Add update tracking
updatePayload.notes = existingTicket.notes +
`\n\n[${new Date().toISOString()}] Auto-updated from ${newLeadData.source}:\n` +
`β Updated: ${fieldsToUpdate.join(', ')}\n` +
`π Preserved: ${fieldsPreserved.join(', ')}`;
const result = await haloApi.put(`/api/Tickets/${existingTicket.id}`, updatePayload);
return {
action: 'updated',
ticket_id: existingTicket.id,
fields_updated: fieldsToUpdate,
fields_preserved: fieldsPreserved,
manual_work_detected: hasManualUpdates || statusChanged
};
}
return {
action: 'skipped',
ticket_id: existingTicket.id,
reason: 'No new data to update',
manual_work_detected: hasManualUpdates || statusChanged
};
}
Lead Import/Update Decision Flow
async function processLeadImport(newLeadData, haloApi) {
// 1. Generate deduplication key
const dedupKey = generateDeduplicationKey(newLeadData);
if (!dedupKey) {
return { action: 'rejected', reason: 'Insufficient data for deduplication' };
}
// 2. Search for existing lead
const existingTicket = await haloApi.get('/api/Tickets', {
params: {
search: newLeadData.email,
tickettype_id: 1, // Lead type
include_custom_fields: '101,102,103,104,105,106,107,108,109,110,111,112,113'
}
});
// 3. Determine action based on existence and status
if (existingTicket && existingTicket.length > 0) {
const ticket = existingTicket[0];
// Check if ticket has progressed beyond initial lead stage
if (ticket.status_id > 2 || ticket.tickettype_id > 1) { // Beyond "Researching" or converted
return {
action: 'preserved',
ticket_id: ticket.id,
reason: 'Lead has progressed in workflow - preserving all data',
current_status: ticket.status_name
};
}
// Perform smart update for early-stage leads
return await smartUpdateLead(ticket, newLeadData, haloApi);
} else {
// 4. Create new lead (original creation logic)
return await createLeadWithListAssignment(newLeadData, haloApi);
}
}
Update Tracking and Audit Trail
// Enhanced logging for update decisions
function logUpdateDecision(result, leadData) {
const logEntry = {
timestamp: new Date().toISOString(),
source: leadData.source,
email: leadData.email,
company: leadData.companyName,
action: result.action,
ticket_id: result.ticket_id,
fields_updated: result.fields_updated || [],
fields_preserved: result.fields_preserved || [],
manual_work_detected: result.manual_work_detected || false
};
// Log to your preferred monitoring system
console.log('Lead Update Decision:', logEntry);
// Optional: Send to webhook for tracking
// await sendToWebhook('lead-update-audit', logEntry);
return logEntry;
}
Bi-Directional Synchronization - Do Not Contact Management
When leads are marked as βDo Not Contactβ in HaloPSA (either manually or through workflow), push this information back to B2B data sources to prevent future re-import and ensure compliance:
// Monitor HaloPSA for DNC flag changes and sync back to source systems
async function syncDoNotContactToSources(ticket, haloApi) {
// Detect DNC flag changes
const dncField = ticket.customfields?.find(f => f.id === 105); // CF_105: Do Not Contact
const leadSource = ticket.customfields?.find(f => f.id === 101)?.value; // CF_101: Lead Source
if (!dncField?.value || !leadSource) return null;
const contactData = {
email: ticket.reportedby,
firstName: ticket.user?.firstname,
lastName: ticket.user?.surname,
companyName: ticket.client?.name,
reason: 'Opted out via HaloPSA workflow',
timestamp: new Date().toISOString(),
halo_ticket_id: ticket.id
};
const suppressionResults = [];
try {
// Push to appropriate B2B platform based on source
switch (leadSource.toLowerCase()) {
case 'apollo.io':
const apolloResult = await addToApolloSuppression(contactData);
suppressionResults.push({source: 'Apollo.io', status: apolloResult.status});
break;
case 'zoominfo':
const zoomResult = await addToZoomInfoSuppression(contactData);
suppressionResults.push({source: 'ZoomInfo', status: zoomResult.status});
break;
case 'hunter.io':
const hunterResult = await addToHunterSuppression(contactData);
suppressionResults.push({source: 'Hunter.io', status: hunterResult.status});
break;
default:
// For unknown sources, log but don't fail
console.log(`Unknown lead source for DNC sync: ${leadSource}`);
}
// Update ticket with suppression status
await haloApi.put(`/api/Tickets/${ticket.id}`, {
notes: ticket.notes +
`\n\n[${new Date().toISOString()}] DNC Suppression Sync:\n` +
suppressionResults.map(r => `β ${r.source}: ${r.status}`).join('\n')
});
return {
success: true,
suppressions_added: suppressionResults,
contact: contactData.email
};
} catch (error) {
console.error('DNC sync failed:', error);
// Log failure but don't block HaloPSA workflow
await haloApi.put(`/api/Tickets/${ticket.id}`, {
notes: ticket.notes +
`\n\n[${new Date().toISOString()}] DNC Suppression Sync FAILED: ${error.message}`
});
return {
success: false,
error: error.message,
contact: contactData.email
};
}
}
B2B Platform Suppression APIs
Apollo.io Suppression
async function addToApolloSuppression(contactData) {
const apolloConfig = {
url: 'https://api.apollo.io/v1/emailer_campaigns/email_accounts/suppression_list',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'X-Api-Key': process.env.APOLLO_API_KEY
}
};
const payload = {
email_account_id: process.env.APOLLO_EMAIL_ACCOUNT_ID,
suppression_list_entries: [{
email: contactData.email,
first_name: contactData.firstName,
last_name: contactData.lastName,
suppression_reason: contactData.reason
}]
};
const response = await fetch(apolloConfig.url, {
method: 'POST',
headers: apolloConfig.headers,
body: JSON.stringify(payload)
});
return {
status: response.ok ? 'Added to suppression list' : `Failed: ${response.statusText}`,
api_response: await response.json()
};
}
ZoomInfo Suppression
async function addToZoomInfoSuppression(contactData) {
const zoomConfig = {
url: 'https://api.zoominfo.com/lookup/suppression',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.ZOOMINFO_ACCESS_TOKEN}`
}
};
const payload = {
emails: [contactData.email],
suppression_reason: contactData.reason,
source: 'HaloPSA_Integration'
};
const response = await fetch(zoomConfig.url, {
method: 'POST',
headers: zoomConfig.headers,
body: JSON.stringify(payload)
});
return {
status: response.ok ? 'Added to suppression list' : `Failed: ${response.statusText}`,
api_response: await response.json()
};
}
Hunter.io Suppression
async function addToHunterSuppression(contactData) {
const hunterConfig = {
url: 'https://api.hunter.io/v2/leads/suppression',
headers: {
'Content-Type': 'application/json'
}
};
const params = new URLSearchParams({
api_key: process.env.HUNTER_API_KEY,
email: contactData.email,
reason: contactData.reason
});
const response = await fetch(`${hunterConfig.url}?${params}`, {
method: 'POST',
headers: hunterConfig.headers
});
return {
status: response.ok ? 'Added to suppression list' : `Failed: ${response.statusText}`,
api_response: await response.json()
};
}
HaloPSA Webhook Trigger for DNC Changes
Set up a HaloPSA webhook to automatically trigger DNC synchronization:
{
"webhook_name": "DNC_Sync_Trigger",
"endpoint": "https://your-integration-server.com/webhooks/dnc-sync",
"triggers": [
{
"entity": "Tickets",
"event": "update",
"conditions": [
{
"field": "customfields.CF_105",
"operator": "changed_to",
"value": true
}
]
},
{
"entity": "Tickets",
"event": "update",
"conditions": [
{
"field": "status_id",
"operator": "changed_to",
"value": 6
}
]
}
]
}
Webhook Handler Implementation
// Webhook endpoint to handle DNC changes from HaloPSA
app.post('/webhooks/dnc-sync', async (req, res) => {
const { ticket_id, event_type, changes } = req.body;
try {
// Fetch full ticket details
const ticket = await haloApi.get(`/api/Tickets/${ticket_id}`, {
params: { include_custom_fields: 'all' }
});
// Check if DNC flag was set
const dncChanged = changes.customfields?.some(cf =>
cf.id === 105 && cf.new_value === true
);
const statusChangedToDNC = changes.status_id?.new_value === 6; // "Do Not Contact" status
if (dncChanged || statusChangedToDNC) {
// Execute bi-directional sync
const result = await syncDoNotContactToSources(ticket, haloApi);
console.log('DNC Sync Result:', result);
// Respond with success
res.json({
success: true,
ticket_id: ticket_id,
suppression_result: result
});
} else {
res.json({
success: true,
message: 'No DNC changes detected'
});
}
} catch (error) {
console.error('Webhook DNC sync failed:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
Compliance and Audit Benefits
- Legal Compliance - Ensures opt-out requests are honored across all platforms
- Cost Efficiency - Prevents wasted credits pulling already-rejected leads
- Data Quality - Improves source data by removing unresponsive contacts
- Audit Trail - Full documentation of suppression actions for compliance
- Real-time Sync - Immediate propagation of DNC decisions via webhooks
Additional Bi-Directional Sync Scenarios
Beyond Do Not Contact management, several other bi-directional sync opportunities provide significant value:
1. Contact Information Updates
// Sync verified contact data back to B2B sources
async function syncContactUpdates(ticket, changes, haloApi) {
const updates = {};
// Phone number verification/updates
if (changes.user?.phonenumber) {
updates.verified_phone = changes.user.phonenumber;
updates.phone_verification_date = new Date().toISOString();
}
// Email verification (bounces, corrections)
if (changes.user?.emailaddress) {
updates.verified_email = changes.user.emailaddress;
updates.email_status = 'verified';
}
// Job title corrections from actual conversations
if (changes.user?.jobtitle) {
updates.corrected_title = changes.user.jobtitle;
updates.title_verification_source = 'direct_contact';
}
return await pushToSourceAPI(updates, ticket.leadSource);
}
2. Company Intelligence Feedback
// Share discovered company insights back to B2B platforms
async function syncCompanyIntelligence(ticket, discoveredData) {
const companyUpdates = {
// Technology stack discoveries during sales conversations
verified_technologies: discoveredData.actualTechStack,
tech_pain_points: discoveredData.technologyChallenges,
// Accurate employee count from direct contact
verified_employee_count: discoveredData.actualEmployeeCount,
employee_count_source: 'sales_conversation',
// Real decision maker structure
decision_makers: discoveredData.identifiedDecisionMakers,
procurement_process: discoveredData.buyingProcess,
// Budget reality vs estimates
actual_budget_range: discoveredData.confirmedBudget,
budget_timeline: discoveredData.budgetCycle,
// Competitive landscape insights
current_vendors: discoveredData.existingVendors,
vendor_satisfaction: discoveredData.vendorFeedback
};
return await enrichSourceDatabase(companyUpdates, ticket.companyId);
}
3. Lead Quality Scoring Feedback
// Provide lead scoring feedback to improve source algorithms
async function syncLeadQualityFeedback(ticket, outcome) {
const qualityMetrics = {
// Conversion tracking
lead_id: ticket.sourceLeadId,
conversion_stage: ticket.currentStage, // Lead β Prospect β Opportunity β Won
conversion_timeline: calculateTimeline(ticket.createdDate, ticket.lastUpdate),
// Quality indicators
data_accuracy_score: calculateDataAccuracy(ticket),
contact_reachability: ticket.contactAttempts?.success_rate,
decision_maker_accuracy: ticket.decisionMakerVerified,
// Sales feedback
sales_feedback: {
lead_quality: ticket.salesTeamRating, // 1-10 scale
data_completeness: ticket.missingDataPoints,
follow_up_potential: ticket.futureOpportunityScore
},
// Outcome tracking
final_outcome: outcome.result, // won, lost, disqualified
outcome_reason: outcome.reason,
deal_value: outcome.dealValue || null
};
return await sendQualityFeedback(qualityMetrics, ticket.leadSource);
}
4. Engagement Activity Sync
// Track engagement activities back to source for better intent scoring
async function syncEngagementData(ticket, activities) {
const engagementData = {
contact_email: ticket.contactEmail,
// Call tracking
calls_made: activities.calls?.length || 0,
calls_answered: activities.calls?.filter(c => c.connected).length || 0,
avg_call_duration: calculateAverageCallTime(activities.calls),
// Email engagement
emails_sent: activities.emails?.length || 0,
emails_opened: activities.emails?.filter(e => e.opened).length || 0,
emails_replied: activities.emails?.filter(e => e.replied).length || 0,
// Meeting outcomes
meetings_scheduled: activities.meetings?.length || 0,
meetings_attended: activities.meetings?.filter(m => m.attended).length || 0,
// Response patterns
best_contact_time: identifyBestContactTime(activities),
preferred_contact_method: identifyPreferredMethod(activities),
response_time_avg: calculateAverageResponseTime(activities)
};
return await updateEngagementProfile(engagementData, ticket.leadSource);
}
5. Industry & Market Intelligence Sharing
// Share market insights discovered during sales process
async function syncMarketIntelligence(ticket, marketData) {
const insights = {
// Industry trends discovered
industry_challenges: marketData.commonPainPoints,
technology_adoption_trends: marketData.techTrends,
budget_allocation_patterns: marketData.spendingPatterns,
// Competitive intelligence
competitor_strengths: marketData.competitorAdvantages,
competitor_weaknesses: marketData.competitorGaps,
pricing_sensitivity: marketData.pricingFeedback,
// Solution effectiveness
successful_use_cases: marketData.winningScenarios,
implementation_challenges: marketData.commonObstacles,
roi_expectations: marketData.expectedOutcomes
};
return await contributeMarketIntelligence(insights, ticket.industry);
}
6. Data Correction & Enrichment
// Correct inaccurate data found during sales process
async function syncDataCorrections(ticket, corrections) {
const dataFixes = {
// Contact corrections
contact_corrections: {
incorrect_email: corrections.emailChanges,
incorrect_phone: corrections.phoneChanges,
incorrect_title: corrections.titleCorrections,
incorrect_department: corrections.departmentChanges
},
// Company corrections
company_corrections: {
incorrect_industry: corrections.industryChanges,
incorrect_size: corrections.sizeCorrections,
incorrect_revenue: corrections.revenueCorrections,
incorrect_headquarters: corrections.locationChanges,
outdated_website: corrections.websiteChanges
},
// Technology corrections
technology_corrections: {
outdated_tech_stack: corrections.techUpdates,
missing_technologies: corrections.additionalTech,
incorrect_platforms: corrections.platformChanges
}
};
return await submitDataCorrections(dataFixes, ticket.leadSource);
}
7. Intent Signal Validation
// Validate and enhance intent signals based on actual conversations
async function syncIntentValidation(ticket, intentData) {
const validatedSignals = {
// Buying intent confirmation
confirmed_buying_intent: intentData.actualBuyingIntent,
buying_timeline_confirmed: intentData.actualTimeline,
budget_intent_validated: intentData.budgetReality,
// Decision process insights
actual_decision_process: intentData.realDecisionProcess,
key_decision_criteria: intentData.decisionFactors,
approval_requirements: intentData.approvalProcess,
// Pain point validation
confirmed_pain_points: intentData.validatedPains,
priority_ranking: intentData.painPriority,
solution_urgency: intentData.urgencyLevel,
// Competitive situation
active_evaluations: intentData.competitorEvaluations,
incumbent_solutions: intentData.currentSolutions,
switching_likelihood: intentData.changeProbability
};
return await validateIntentSignals(validatedSignals, ticket.contactId);
}
Implementation Strategy for Multiple Sync Types
// Unified bi-directional sync orchestrator
class BiDirectionalSyncManager {
constructor(haloApi, b2bConfigs) {
this.haloApi = haloApi;
this.b2bConfigs = b2bConfigs;
this.syncQueue = [];
}
async processTicketUpdate(ticket, changes) {
const syncTasks = [];
// Determine which sync types apply
if (this.isDNCUpdate(changes)) {
syncTasks.push(this.syncDoNotContact(ticket));
}
if (this.isContactUpdate(changes)) {
syncTasks.push(this.syncContactUpdates(ticket, changes));
}
if (this.isCompanyDiscovery(changes)) {
syncTasks.push(this.syncCompanyIntelligence(ticket, changes));
}
if (this.isOutcomeUpdate(changes)) {
syncTasks.push(this.syncLeadQualityFeedback(ticket, changes));
}
if (this.isEngagementActivity(changes)) {
syncTasks.push(this.syncEngagementData(ticket, changes));
}
// Execute all applicable sync tasks
return await Promise.allSettled(syncTasks);
}
isDNCUpdate(changes) {
return changes.customfields?.some(cf => cf.id === 105) ||
changes.status_id === 6;
}
isContactUpdate(changes) {
return changes.user?.emailaddress ||
changes.user?.phonenumber ||
changes.user?.jobtitle;
}
isCompanyDiscovery(changes) {
return changes.customfields?.some(cf =>
[106, 107, 108, 109, 110].includes(cf.id)
);
}
isOutcomeUpdate(changes) {
return changes.status_id > 10 || // Advanced status
changes.tickettype_id > 1; // Converted to Prospect/Opportunity
}
isEngagementActivity(changes) {
return changes.actions?.length > 0 || // New activities
changes.last_contact_date; // Engagement tracking
}
}
Benefits of Comprehensive Bi-Directional Sync
- Data Quality Improvement - Real-world validation improves source data accuracy
- Algorithm Enhancement - Lead scoring and intent models benefit from outcome feedback
- Cost Optimization - Better data reduces wasted outreach and improves ROI
- Competitive Intelligence - Market insights benefit entire user community
- Sales Efficiency - More accurate data means better targeting and higher conversion
- Compliance Management - Comprehensive contact preference tracking
- Revenue Attribution - Clear tracking from lead source to closed deals
Workflow Automation Rules
- Auto-promotion: Leads with status βEngagedβ can be auto-converted to Prospects
- Data inheritance: All data carries forward during entity promotion
- Status validation: Ensure status changes follow defined workflow paths
- Required fields: Validate required attributes before allowing status progression
API Endpoint Mapping
HaloPSA API Endpoints & Entity Structure
Based on official HaloPSA API documentation (https://haloacademy.halopsa.com/apidoc/info), here are the correct API endpoints for CRM workflow integration:
Core Entity Endpoints
- Tickets (Leads/Prospects) β
POST/GET/PUT /api/Tickets - Users (Contacts) β
POST/GET/PUT /api/Users - Clients (Organizations) β
POST/GET/PUT /api/Client - Sites (Locations) β
POST/GET/PUT /api/Site - Opportunities β
POST/GET/PUT /api/Opportunities - Actions (Calls/Activities) β
POST/GET/PUT /api/Actions
List Management Endpoints
- Lists β
GET /api/Lists- Retrieve all available lists - List Items β
GET /api/Lists/{listId}/items- Get items in specific list - Add to List β
POST /api/Lists/{listId}/items- Add tickets to list - Remove from List β
DELETE /api/Lists/{listId}/items/{itemId}- Remove from list
List Organization Strategy for B2B Leads
Recommended List Structure
{
"lead_source_lists": {
"apollo_leads": "Apollo.io Leads",
"zoominfo_leads": "ZoomInfo Leads",
"hunter_leads": "Hunter.io Leads",
"uplead_leads": "UpLead Leads",
"lusha_leads": "Lusha Leads"
},
"qualification_lists": {
"hot_leads": "Hot Leads (Fit Score >80)",
"warm_leads": "Warm Leads (Fit Score 60-80)",
"cold_leads": "Cold Leads (Fit Score <60)",
"dnc_list": "Do Not Contact"
},
"industry_lists": {
"healthcare_leads": "Healthcare & Medical",
"legal_leads": "Legal Services",
"financial_leads": "Financial Services",
"manufacturing_leads": "Manufacturing",
"retail_leads": "Retail & E-commerce"
},
"size_based_lists": {
"enterprise_leads": "Enterprise (500+ employees)",
"mid_market_leads": "Mid-Market (50-499 employees)",
"smb_leads": "Small Business (1-49 employees)"
}
}
Auto-Assignment Logic
async function assignLeadToLists(leadData, haloApi) {
const assignments = [];
// Source-based assignment
const sourceList = await getListBySource(leadData.leadSource);
if (sourceList) assignments.push(sourceList.id);
// Qualification-based assignment
const fitScore = leadData.customFields.find(f => f.id === 206)?.value || 0;
if (fitScore > 80) assignments.push(await getListId('hot_leads'));
else if (fitScore >= 60) assignments.push(await getListId('warm_leads'));
else assignments.push(await getListId('cold_leads'));
// Industry-based assignment
const industry = leadData.customFields.find(f => f.id === 102)?.value;
const industryList = await getListByIndustry(industry);
if (industryList) assignments.push(industryList.id);
// Size-based assignment
const employeeCount = leadData.customFields.find(f => f.id === 108)?.value;
const sizeList = await getListByCompanySize(employeeCount);
if (sizeList) assignments.push(sizeList.id);
// Do Not Contact check
const dncFlag = leadData.customFields.find(f => f.id === 105)?.value;
if (dncFlag) assignments.push(await getListId('dnc_list'));
return assignments;
}
Authentication & Rate Limiting
- OAuth2 Required - All endpoints require Bearer token authentication
- Rate Limit: 500 requests per 5-minute rolling window
- Base URL:
https://yourtenant.halopsa.com/api - Authorization:
https://yourtenant.halopsa.com/auth
Custom Fields & Lookups
- Custom Fields β Use
include_custom_fieldsparameter in GET requests - Ticket Types β
GET /api/TicketType - Ticket Statuses β
GET /api/Status
Entity Creation Workflow
Lead Creation (As Ticket)
POST /api/Tickets
Authorization: Bearer {access_token}
Content-Type: application/json
{
"tickettype_id": 1,
"summary": "{firstName} {lastName} - {companyName}",
"details": "Lead imported from {source} on {date}",
"status_id": 1,
"priority_id": 3,
"category_1": "Lead",
"category_2": "{source}",
"user_id": null,
"client_id": null,
"customfields": [
{"id": 101, "value": "{leadSource}"},
{"id": 102, "value": "{servicesOffered}"},
{"id": 103, "value": "{growthSignals}"},
{"id": 104, "value": "{projectPipelines}"},
{"id": 105, "value": false}
]
}
Automatic List Assignment After Lead Creation
// After creating the lead ticket, assign to appropriate lists
async function createLeadWithListAssignment(leadData, haloApi) {
// 1. Create the lead ticket
const ticket = await haloApi.post('/api/Tickets', {
tickettype_id: 1,
summary: `${leadData.firstName} ${leadData.lastName} - ${leadData.companyName}`,
details: `Lead imported from ${leadData.source} on ${new Date().toISOString()}`,
status_id: 1,
priority_id: 3,
category_1: "Lead",
category_2: leadData.source,
customfields: [
{"id": 101, "value": leadData.source},
{"id": 102, "value": leadData.servicesOffered},
{"id": 103, "value": leadData.growthSignals},
{"id": 104, "value": leadData.projectPipelines},
{"id": 105, "value": leadData.doNotContact},
{"id": 106, "value": leadData.technologyStack},
{"id": 107, "value": leadData.revenueRange},
{"id": 108, "value": leadData.employeeCountRange},
{"id": 109, "value": leadData.managementLevel},
{"id": 110, "value": leadData.departmentFunction},
{"id": 111, "value": leadData.intentSignals},
{"id": 112, "value": leadData.foundedYear},
{"id": 113, "value": leadData.hqAddress}
]
});
// 2. Determine appropriate lists
const listAssignments = await assignLeadToLists(leadData, haloApi);
// 3. Add ticket to each relevant list
for (const listId of listAssignments) {
await haloApi.post(`/api/Lists/${listId}/items`, {
ticket_id: ticket.id,
added_by: "B2B Integration",
notes: `Auto-assigned based on: ${leadData.source} source`
});
}
return {
ticket_id: ticket.id,
assigned_lists: listAssignments,
summary: `Lead created and assigned to ${listAssignments.length} lists`
};
}
List Retrieval and Management
GET /api/Lists
Authorization: Bearer {access_token}
Response:
{
"lists": [
{
"id": 1,
"name": "Apollo.io Leads",
"description": "Leads sourced from Apollo.io",
"item_count": 156,
"created_date": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"name": "Hot Leads (Score >80)",
"description": "High-quality leads with fit score above 80",
"item_count": 23,
"created_date": "2024-01-01T00:00:00Z"
}
]
}
Add Ticket to List
POST /api/Lists/{listId}/items
Authorization: Bearer {access_token}
Content-Type: application/json
{
"ticket_id": 12345,
"added_by": "B2B Integration System",
"notes": "Auto-assigned based on source and qualification criteria"
}
Contact/User Creation
POST /api/Users
Authorization: Bearer {access_token}
Content-Type: application/json
{
"firstname": "{firstName}",
"surname": "{lastName}",
"emailaddress": "{email}",
"jobtitle": "{jobTitle}",
"phonenumber": "{phone}",
"site_id": "{siteId}",
"inactive": false,
"isimportantcontact": true
}
Organization/Client Creation
POST /api/Client
Authorization: Bearer {access_token}
Content-Type: application/json
{
"name": "{companyName}",
"website": "{website}",
"phonenumber": "{phone}",
"notes": "Imported from {source}",
"inactive": false
}
Site Creation (Company Location)
POST /api/Site
Authorization: Bearer {access_token}
Content-Type: application/json
{
"name": "{companyName} - Main Office",
"client_id": "{clientId}",
"phonenumber": "{phone}",
"website": "{website}",
"inactive": false
}
Relationship Linking
After creating core entities, establish relationships using HaloPSA hierarchy:
// 1. Create Client (Organization)
const client = await fetch('/api/Client', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(organizationData)
});
// 2. Create Site (Primary location)
const site = await fetch('/api/Site', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({...siteData, client_id: client.id})
});
// 3. Create User (Contact)
const user = await fetch('/api/Users', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({...userData, site_id: site.id})
});
// 4. Create Ticket (Lead)
const ticket = await fetch('/api/Tickets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
...ticketData,
client_id: client.id,
user_id: user.id
})
});
Status Progression API Calls
Lead Status Update
PUT /api/Tickets/{ticketId}
Authorization: Bearer {access_token}
Content-Type: application/json
{
"status_id": 2, // Update to "Researching"
"agent_id": "{assignedAgent}",
"customfields": [
{"id": 106, "value": "Updated via workflow automation"}
]
}
Convert Lead to Prospect (Status + Type Change)
PUT /api/Tickets/{ticketId}
Authorization: Bearer {access_token}
Content-Type: application/json
{
"tickettype_id": 2, // Change to Prospect ticket type
"status_id": 10, // New Prospect status
"category_1": "Prospect",
"customfields": [
{"id": 201, "value": "{confirmedPainPoints}"},
{"id": 202, "value": "{qualifiedServices}"},
{"id": 203, "value": "{decisionMaker}"},
{"id": 204, "value": "{budgetRange}"},
{"id": 205, "value": "{timeframe}"},
{"id": 206, "value": 75}
]
}
Promote to Opportunity
POST /api/Opportunities
Authorization: Bearer {access_token}
Content-Type: application/json
{
"summary": "{companyName} - {opportunityName}",
"client_id": "{clientId}",
"user_id": "{contactUserId}",
"details": "Promoted from qualified prospect",
"customfields": [
{"id": 301, "value": "{productsServices}"},
{"id": 302, "value": "{quotesProposals}"},
{"id": 303, "value": "{competitors}"},
{"id": 304, "value": ""}
]
}
Filtering & Querying
Get Leads by Status
GET /api/Tickets?status_id=1&tickettype_id=1&include_custom_fields=101,102,103,104,105
Authorization: Bearer {access_token}
Get Prospects by Client
GET /api/Tickets?client_id=123&tickettype_id=2&include_custom_fields=201,202,203,204,205,206
Authorization: Bearer {access_token}
Get Open Opportunities
GET /api/Opportunities?open_only=true&include_custom_fields=301,302,303,304
Authorization: Bearer {access_token}
Bulk Operations
HaloPSA supports bulk operations by posting arrays:
POST /api/Tickets
Authorization: Bearer {access_token}
Content-Type: application/json
[
{/* ticket 1 data */},
{/* ticket 2 data */},
{/* ticket 3 data */}
]
Use ?bulkresponse=true parameter to get individual response status for each object.
Error Handling
Common HTTP status codes:
- 200: Success
- 201: Created
- 400: Bad Request (validation errors)
- 401: Unauthorized (invalid token)
- 403: Forbidden (insufficient permissions)
- 429: Too Many Requests (rate limit exceeded)
- 500: Internal Server Error
This configuration uses the official HaloPSA API endpoints for managing the complete customer acquisition lifecycle from lead discovery through closed opportunities.