Arsalan Faysal – Automation & RevOps Blog

Zero-Tolerance CRM Sync: A HubSpot and Business Central Case Study

Written by Arsalan Faysal | May 27, 2026 7:44:50 AM
4 Integration Pillars Architected
5 Match Hierarchy Layers
15 Min Max Acceptable Sync Lag (BC → HS)
0 Silent Data Drops Permitted

A 60-year-old company that has survived six decades by being operationally serious does not need a CRM philosophy lecture. It needs an architecture that works without the sales team having to think about it. The brief is clear and the constraint set is honest: the people using this system are comfortable with email and phone, not with modern CRM interfaces. If a rep has to manually log a call, the system has already failed. Automation is not a feature enhancement here — it is the entire premise. Every touchpoint — call, email, Teams meeting, web form — must attach to the right account automatically, handle ambiguity intelligently rather than silently, and be maintainable by a non-developer after handoff.

This post documents the full architecture for rebuilding that capability on HubSpot CRM: the Business Central sync design, the Outlook two-way logging configuration, the Teams meeting link workflow, the Dialpad AI summary pipeline, and the matching and reconciliation layer that handles every case where the data does not map cleanly. This is not a survey of options. It is a specific, opinionated architecture built around the constraint that zero tolerance for silent data drops and zero tolerance for engineering dependency after handoff are both non-negotiable requirements — and those two requirements must be satisfied simultaneously.

Every design decision below is made in that context. The choice between native connector and middleware is never aesthetic — it is driven by which option maintains the no-silent-drop guarantee and which one a non-developer can diagnose and correct when something goes wrong at 9am on a Tuesday.

•   •   •

Object Schema and HubSpot Configuration — Build the Foundation Before Any Integration Runs

Every integration failure in a HubSpot implementation traces back to one of two causes: a mapping was wrong, or the object it was mapping to did not exist in the right form. The object schema is not a configuration task you complete alongside the integrations. It is the prerequisite that must be stable before the first integration is connected. If Business Central starts pushing Company records before the HubSpot Company object has the custom properties that map to BC's Customer fields, the sync either drops data silently or creates messy workarounds that accumulate as technical debt in the property definitions.

Custom Property Architecture — The Full Schema for This Build

HubSpot's native Company and Contact objects cover the standard fields. This build requires a specific set of custom properties that map to Business Central's data model, Dialpad's call record structure, and the matching layer's confidence scoring system. These properties must be created before any integration is activated.

Object Property Name Type Purpose / Source System
Company bc_customer_id Single-line text Business Central Customer No. — primary match key for BC → HubSpot sync. Immutable after first write. Never overwritten by BC sync after creation.
Company bc_customer_status Dropdown BC customer status: Active / Blocked / On Hold. Synced from BC. Values controlled by BC — HubSpot team cannot edit.
Company bc_last_sync_date Date Timestamp of last successful BC → HubSpot sync for this record. Used by monitoring workflow to flag stale records.
Company bc_total_order_value_ytd Number (currency) Year-to-date order value from BC Sales Orders. Synced — not editable by HubSpot team.
Company bc_open_quote_count Number Count of open Sales Quotes in BC for this customer. Synced. Drives HubSpot deal pipeline activity alerts.
Company match_confidence_score Number (0–100) Confidence score assigned by matching layer. Records below 70 are flagged for review before data is committed. Set by Make.com matching scenario.
Company match_review_flag Checkbox True when matching confidence is below threshold. Drives review queue Smart List. Cleared manually by admin after review.
Contact phone_e164 Single-line text Normalized phone number in E.164 format (+[country][number]). Primary match key for Dialpad call matching. Populated on contact creation by normalization workflow.
Contact dialpad_contact_id Single-line text Dialpad's internal contact identifier. Written on first successful Dialpad match. Used for subsequent call logging without re-running phone match.
Contact last_dialpad_call_date Date Date of most recent Dialpad call logged to this contact. Used in engagement reporting and lifecycle stage workflows.
Contact last_dialpad_call_direction Dropdown Inbound / Outbound. Set by Dialpad webhook payload field direction.
Contact unmatched_activity_flag Checkbox True when a call or email arrived that could not be matched to this contact confidently. Triggers review queue task creation.
Deal bc_quote_id Single-line text Business Central Sales Quote No. Links HubSpot Deal to BC source document. Immutable after first write.
Deal bc_order_id Single-line text Business Central Sales Order No. Written when a Quote converts to Order. Drives deal stage automation.
Deal bc_order_amount Number (currency) Order value from BC. Synced — not editable in HubSpot. Feeds deal amount field for pipeline reporting.

Native Deals vs. Custom Objects — The Recommendation for This Build

The choice between HubSpot's native Deal object and custom objects for Sales Orders and Sales Quotes from Business Central is a question with a specific answer for this architecture. The recommendation is: use native Deals for Sales Quotes (where HubSpot's pipeline and deal stage machinery adds value), and use a custom object for Sales Orders (where the order is a financial record that belongs to the BC system of record, not a sales motion in HubSpot).

The reasoning: HubSpot's Deal object is built around a pipeline with stages, probability weighting, and close date — features that are meaningful for a Sales Quote moving through a sales process toward conversion. When a quote is open in BC, a corresponding Deal in HubSpot's pipeline allows the sales team to track follow-up, log calls, and move the deal through stages that reflect the actual sales motion. When the quote converts to a Sales Order in BC, the Deal moves to Closed Won, and a separate custom object record — the Sales Order — is created to hold the financial record without polluting the deal pipeline with non-pipeline records.

OBJECT SCHEMA DECISION — DEALS VS. CUSTOM OBJECTS FOR BC DATA
DECISION 01
BC Sales Quotes → HubSpot Native Deals (Recommended) Sales Quotes have a sales lifecycle — they are created, followed up on, revised, and either converted or lost. HubSpot's native Deal pipeline machinery (stages, tasks, sequences, deal-based workflows) is exactly the right operational layer for this motion. Each BC Quote maps to one HubSpot Deal. bc_quote_id on the Deal prevents duplicate creation on re-sync.
DECISION 02
BC Sales Orders → HubSpot Custom Object: "Sales Order" (Recommended) Sales Orders are financial records, not sales motions. They belong to BC as the system of record. In HubSpot, they exist as reference records associated with the Company object — giving the sales team visibility into order history without running order management inside a CRM pipeline. Custom object allows clean association: Company → has many Sales Orders. No pipeline stages, no deal probability — just financial data from BC displayed in context.
DECISION 03
BC Customers → HubSpot Companies (Native — no custom object needed) HubSpot's Company object natively maps to BC's Customer entity. The custom properties above (bc_customer_id, bc_customer_status, bc_total_order_value_ytd) extend the native Company object to hold BC-specific data. No custom object required — the native Company object with custom properties is the correct architecture.
•   •   •

Pillar One — Business Central → HubSpot: The One-Way Sync Architecture

Business Central is the system of record for financial and customer master data. HubSpot is the system of record for relationship activity. The sync between them is intentionally one-directional — BC data flows into HubSpot, never the reverse. A HubSpot team member adding notes or updating relationship fields does not push anything back to BC. This is a design constraint that protects the financial integrity of the BC data while allowing the HubSpot team to enrich records with relationship context without risk of corrupting the source system.

Connector Selection — Native vs. Middleware for Business Central

Business Central does not have a native HubSpot integration. There is no first-party connector that syncs BC Customers, Sales Orders, and Sales Quotes to HubSpot out of the box. The integration options are: a certified marketplace connector (Dataverse/Power Automate approach via Microsoft's ecosystem, or a commercial BC-HubSpot connector like those from Alumio or Layer2), a Make.com scenario set using BC's OData API and HubSpot's CRM API, or a custom webhook handler built on a serverless platform.

The recommendation for this build is Make.com with BC's OData API. The rationale is the handoff constraint: the system must be maintainable by a non-developer after handoff. Make.com's visual scenario builder is the most accessible middleware platform for non-developers who need to diagnose a failed sync run, understand why a record did not create, or add a field to the mapping without requiring a developer engagement. Commercial BC-HubSpot connectors have the advantage of pre-built mappings but introduce a third-party dependency whose pricing, reliability, and support quality is outside the control of either BC or HubSpot — and whose failure mode is typically a black box with limited visibility into why a specific record failed.

BUSINESS CENTRAL → HUBSPOT SYNC ARCHITECTURE (Make.com + OData API)
──────────────────────────────────────────────────────────────────

SYNC SCENARIO 1: BC CUSTOMER → HUBSPOT COMPANY
  Schedule: Every 10 minutes (within 15-minute acceptable lag window)
  
  Step 1: BC OData API — GET Customers modified in last 12 minutes
    Endpoint: /api/v2.0/companies/{bc_company_id}/customers
    Filter: $filter=lastModifiedDateTime gt {datetime_minus_12_min}
    Fields: No., Name, Address, City, Country, Phone, Email,
            Blocked, LastModifiedDateTime
  
  Step 2: For each Customer record — search HubSpot
    Search by: bc_customer_id = Customer.No.
    → IF match found: PATCH Company — update allowed fields only
      Allowed to overwrite: Name, Address, Phone, bc_customer_status,
                            bc_last_sync_date
      NEVER overwrite: Notes, HubSpot team-added properties, 
                       owner assignment, lifecycle stage
    → IF no match found: POST new Company
      Set: bc_customer_id, name, address, phone, email, bc_customer_status
      Do NOT set: any HubSpot relationship properties (set by team)
    
  Step 3: Log sync result
    → Success: update bc_last_sync_date on Company record
    → Failure: write to Make.com error log + send alert email to admin
    
  Error Handling:
    → BC OData 429 (rate limit): exponential backoff, retry 3x
    → HubSpot API 429: pause scenario 60 seconds, retry
    → Duplicate Company detected (same bc_customer_id, different hs_id):
      Alert admin — do NOT auto-merge, flag for manual review

──────────────────────────────────────────────────────────────────

SYNC SCENARIO 2: BC SALES QUOTE → HUBSPOT DEAL
  Schedule: Every 10 minutes
  
  Step 1: BC OData API — GET Sales Quotes modified in last 12 minutes
    Endpoint: /api/v2.0/companies/{bc_company_id}/salesQuotes
    Filter: $filter=lastModifiedDateTime gt {datetime_minus_12_min}
    Fields: id, No., customerNo., customerName, status, 
            totalAmountExclTax, validUntilDate, lastModifiedDateTime
  
  Step 2: Find associated HubSpot Company
    Search by: bc_customer_id = Quote.customerNo.
    → IF Company not found: flag quote as unmatched — create review task
    
  Step 3: Search HubSpot Deals for existing quote
    Search by: bc_quote_id = Quote.No.
    → IF Deal exists: PATCH deal amount, status, validUntilDate
    → IF no Deal: POST new Deal
      Pipeline: Sales Pipeline
      Stage: determined by BC Quote status mapping (see table below)
      Associate: to matched Company record
      Set: bc_quote_id, bc_order_amount (from totalAmountExclTax)
  
  Step 4: BC Quote Status → HubSpot Deal Stage Mapping
    BC Status: Draft       → HS Stage: Quote Drafted
    BC Status: Sent        → HS Stage: Proposal Sent
    BC Status: Accepted    → HS Stage: Closed Won (triggers BC Order sync)
    BC Status: Cancelled   → HS Stage: Closed Lost

──────────────────────────────────────────────────────────────────

SYNC SCENARIO 3: BC SALES ORDER → HUBSPOT CUSTOM OBJECT
  Schedule: Every 10 minutes
  Trigger condition: New Sales Order OR Order amount changed
  
  Step 1: GET Sales Orders modified in last 12 minutes (same OData pattern)
  Step 2: Find associated HubSpot Company by bc_customer_id
  Step 3: Search existing Sales Order custom object records by bc_order_id
    → IF exists: PATCH amount, status fields
    → IF new: POST Sales Order custom object record
      Associate to Company record
      Fields: bc_order_id, bc_order_amount, order_date, status
  Step 4: Update Company's bc_total_order_value_ytd 
    (calculated from all associated Sales Order records)

Field Protection — Preventing BC from Overwriting HubSpot Team Data

The most operationally critical design detail in the BC sync is the field protection layer. The sync runs every 10 minutes. Without explicit field protection, every sync run would overwrite any changes the HubSpot team made to a Company record since the last sync — notes added by account managers, owner reassignments made by the sales director, lifecycle stage updates triggered by relationship events. This would make HubSpot unusable for relationship management, because every edit would be overwritten within 10 minutes.

The field protection is implemented at the Make.com scenario level, not at the HubSpot level. The PATCH operation sent to HubSpot from the BC sync scenario includes only the fields in the "allowed to overwrite" list defined above. It does not include a payload for HubSpot-owned fields. Since HubSpot's PATCH endpoint only updates the fields present in the payload, any field not included in the BC sync payload retains its current value regardless of how many times the sync runs. This is the correct architecture — it requires no HubSpot configuration to enforce and no per-field locking mechanism that adds administrative overhead.

•   •   •

Pillar Two — Outlook ↔ HubSpot: Automatic Two-Way Email Logging

Outlook integration is where HubSpot's native capabilities are strongest, and where the no-silent-drop requirement requires the most careful configuration. HubSpot's native Outlook add-in and the connected inbox feature both exist — but they have different behavior profiles, and the choice between them has significant consequences for the "no manual effort" constraint in this build.

Connected Inbox vs. Add-In — The Correct Choice for This Architecture

HubSpot offers two Outlook integration modes. The first is the Sales Extension (Add-In): a panel that appears in Outlook, allowing reps to manually log emails, see CRM context, and use sequences — but requiring deliberate action for each email they want to log. The second is the Connected Inbox: an account-level connection that automatically logs all emails sent to or received from known HubSpot contacts, with no manual action required per email.

For this build, Connected Inbox is the only viable option. The add-in requires a manual action per email — exactly the pattern the brief explicitly rules out. Connected Inbox logs automatically based on email address matching against the HubSpot Contact database. The configuration decision is which emails get logged and what happens when no match is found.

OUTLOOK CONNECTED INBOX CONFIGURATION — NO-SILENT-DROP SETTINGS
CONFIG 01
Auto-log all sent emails: ON Every email sent from a connected rep's Outlook account to an address that matches a HubSpot Contact's email is automatically logged to that contact's timeline. No manual action. The "From" address on the HubSpot activity record reflects the sending rep, ensuring activity is attributed to the correct owner.
CONFIG 02
Auto-log all received emails: ON (with domain exclusion list) Inbound emails from known contacts are logged automatically. A domain exclusion list is configured to prevent internal emails (company domain), newsletter services, and no-reply addresses from creating junk activity records. Exclusion list: @[companydomain].com, noreply@*, notifications@*, donotreply@*. Maintained by admin in HubSpot connected inbox settings — no developer required.
CONFIG 03
Unmatched email handling: Flag-for-review workflow (NOT silent drop) When an email arrives from an address with no matching HubSpot Contact, HubSpot's default behavior is to not log it. To enforce the no-silent-drop requirement, a HubSpot workflow monitors the connected inbox for emails without contact association and creates a review task: "Unmatched email received from [sender address] — create contact or confirm not a prospect." Task assigned to the rep who sent or received the email. Task due: same business day.
CONFIG 04
Email thread association: Automatic by email address chain HubSpot's connected inbox associates email threads to the same contact record for the duration of the thread, even if participants are added mid-thread. Threads involving multiple known contacts are logged to all matched records. Threads involving a mix of known and unknown addresses are logged to matched records and flagged for the unknown addresses — no partial silent drop.
OUTLOOK EMAIL LOGGING FLOW — EVERY SCENARIO HANDLED
──────────────────────────────────────────────────────────────────

INBOUND EMAIL RECEIVED BY REP
  │
  ├── Sender address matches HubSpot Contact email?
  │     YES → Auto-log to Contact timeline ✓
  │           Associate to Company if contact has company association ✓
  │           No manual action required
  │
  └── Sender address NOT in HubSpot?
        │
        ├── Address on domain exclusion list?
        │     YES → Ignore (internal / newsletter / no-reply) ✓
        │
        └── NOT on exclusion list?
              → Flag-for-review task created:
                Assigned to: receiving rep
                Task: "Unknown sender [email] — create contact or dismiss"
                Due: Today (same business day)
                NO silent drop ✓

OUTBOUND EMAIL SENT BY REP
  │
  ├── Recipient address matches HubSpot Contact email?
  │     YES → Auto-log to Contact timeline ✓
  │           Activity attributed to sending rep as owner ✓
  │
  └── Recipient NOT in HubSpot?
        → Flag-for-review task:
          "Sent email to unrecognized address [email] — log contact?"
          Option: One-click contact creation from task interface
          NO silent drop ✓
•   •   •

Pillar Three — Microsoft Teams → HubSpot: Meeting Recording Association

Teams integration is the pillar where the brief's explicit acceptance of a semi-automated Phase 1 approach is the correct posture. Full automation of Teams recording association — where every Teams meeting is automatically linked to the right HubSpot contact without any manual intervention — requires a level of integration complexity that is not proportional to the value delivered in Phase 1, and it introduces maintenance surface that conflicts with the non-developer handoff requirement.

The Phase 1 architecture is a structured manual workflow supported by HubSpot's meeting logging feature, with automation handling the parts that can be automated cleanly and manual action limited to the one step that requires human judgment: confirming which HubSpot record a Teams meeting should be associated with.

Phase 1 — Structured Semi-Automated Teams Logging

The Teams integration uses HubSpot's native meeting logging capability combined with a Make.com scenario that handles the mechanical parts of the association process. The rep's only required action is a single click to confirm the HubSpot record association — not a data entry task, not a copy-paste operation, not a navigation to the contact record.

TEAMS MEETING → HUBSPOT ASSOCIATION WORKFLOW (Phase 1)
──────────────────────────────────────────────────────────────────

TRIGGER: Teams meeting ends
  → Microsoft Graph API webhook fires: meeting.ended event
  → Payload: meeting_id, organizer, attendees (email list), 
             duration, recording_url (if recording enabled),
             transcript_url (if transcription enabled)

STEP 1 — MAKE.COM SCENARIO: Identify HubSpot matches for attendees
  → For each attendee email in meeting attendees list:
      Search HubSpot Contacts: email = attendee.email
      Exclude: internal email domain (own company)
      Result: list of matched HubSpot Contact IDs and Names
  
  → IF exactly one external contact matched:
      Pre-populate association record — high confidence
  → IF multiple external contacts matched:
      List all matches for rep to confirm
  → IF no external contact matched:
      Flag: "Meeting attendee not in HubSpot — create contact?"

STEP 2 — REP NOTIFICATION (Slack or Email):
  Message to meeting organizer:
  "You had a Teams meeting with [Attendee Name(s)].
   Link this meeting to their HubSpot record?
   → [YES — Link to Contact Name] (one click)
   → [DIFFERENT CONTACT] (opens HubSpot search)
   → [INTERNAL ONLY — No CRM record needed] (dismiss)"

STEP 3 — ON REP CONFIRMATION:
  → Make.com: POST HubSpot Engagement (Meeting activity type)
    Properties: meeting_title (from Teams), 
                duration (from Graph API),
                meeting_date, 
                meeting_outcome: "Completed — see recording",
                recording_url (if available),
                attendees: [internal rep + contact name]
    Association: Contact ID + Company ID (from contact association)
  → Activity appears on Contact timeline ✓
  → Recording URL clickable from HubSpot contact record ✓

STEP 4 — IF REP DISMISSES (no action within 24h):
  → Create review task: "Unlogged Teams meeting on [date] — 
     confirm if CRM record needed"
  → Assigned to rep's manager
  → NO silent drop ✓

──────────────────────────────────────────────────────────────────

PHASE 2 UPGRADE PATH (when warranted by volume):
  → Microsoft Graph API: subscribe to calendar event changes
  → Pre-create HubSpot meeting record when meeting is SCHEDULED
    (before it occurs) based on calendar invitees
  → Post-meeting: auto-attach recording URL to pre-created record
  → Reduce rep action to zero for meetings where invitees are 
    already known HubSpot contacts
  → Estimated Phase 2 scope: 15–20 additional Make.com steps
•   •   •

Pillar Four — Dialpad ↔ HubSpot: The High-Priority Integration

Dialpad is the highest-priority integration in the brief and the one with the most architectural nuance. The brief correctly identifies that Dialpad's native HubSpot integration may not fully cover the AI summary push and smart matching requirements — and that read is accurate. Dialpad's native integration handles basic call logging: it creates a HubSpot call activity for calls made to or from known HubSpot contacts, captures duration, direction, and date. What it does not reliably handle in all account configurations is: pushing Dialpad's AI-generated call summary as a structured note on the HubSpot activity record, handling unmatched phone numbers with a review workflow rather than a silent drop, and providing phone number normalization to E.164 format before the match attempt.

The architecture recommendation: use Dialpad's native integration as the first-pass logging layer for matched calls, and layer a Make.com webhook handler on top for AI summary push, unmatched number handling, and phone normalization. This hybrid approach means matched calls get double coverage — native integration logs the call, Make.com enriches it with the AI summary — and unmatched calls are caught by the Make.com layer that the native integration would have silently dropped.

Dialpad Webhook Architecture — Full Call Event Pipeline

DIALPAD CALL EVENT → HUBSPOT PIPELINE (Make.com Middleware Layer)
──────────────────────────────────────────────────────────────────

TRIGGER: Dialpad webhook fires on call.ended event
  Payload received by Make.com webhook URL:
  {
    "call_id": "dp_call_abc123",
    "direction": "inbound" | "outbound",
    "duration_seconds": 342,
    "started_at": "2025-10-14T14:22:00Z",
    "ended_at": "2025-10-14T14:27:42Z",
    "from_number": "+13125559012",
    "to_number": "+17085554321",
    "rep_email": "rep.name@company.com",
    "recording_url": "https://dialpad.com/recordings/...",
    "ai_summary": "Customer called to follow up on Quote #QU-00234. 
                   Discussed delivery timeline. Customer requested 
                   updated pricing. Rep agreed to send revised quote 
                   by Friday.",
    "ai_action_items": ["Send revised quote by Friday",
                        "Confirm delivery timeline with ops team"],
    "sentiment": "positive"
  }

──────────────────────────────────────────────────────────────────

STEP 1 — PHONE NUMBER IDENTIFICATION
  Determine the external number (non-company number):
  → IF direction = inbound: external_number = from_number
  → IF direction = outbound: external_number = to_number
  Normalize to E.164 format: strip spaces, dashes, parentheses,
  add country code if missing (default: +1 for US accounts)

STEP 2 — HUBSPOT CONTACT MATCH (four-pass hierarchy)
  
  Pass 1 — Exact E.164 match on phone_e164 custom property:
    HubSpot Search API: phone_e164 = {external_number_e164}
    → IF match: proceed to Step 3 with matched contact_id
  
  Pass 2 — HubSpot native phone field match (multiple format variants):
    Search: phone = {external_number} 
            OR phone = {local_format}   (without country code)
            OR mobilephone = {external_number_e164}
    → IF match: proceed to Step 3 AND update phone_e164 on contact
  
  Pass 3 — Dialpad contact ID match:
    Search: dialpad_contact_id = {dialpad_contact_id from payload}
    (covers cases where Dialpad knows the contact but phone changed)
    → IF match: proceed to Step 3
  
  Pass 4 — No match found:
    → DO NOT create call activity on a random record
    → DO NOT silently drop
    → CREATE review task: "Unmatched call — [direction], [duration]s, 
       from [external_number] — identify contact and log manually"
    → Assign to rep_email (from payload)
    → Due: same business day
    → Log to unmatched_calls audit table (Airtable or Google Sheets)
    → Proceed to UNMATCHED FLOW (see below)

STEP 3 — CREATE HUBSPOT CALL ACTIVITY (matched contact)
  POST /crm/v3/objects/calls
  Properties:
    hs_call_direction: {direction}
    hs_call_duration: {duration_seconds × 1000}  (HS uses milliseconds)
    hs_call_status: "COMPLETED"
    hs_timestamp: {started_at}
    hs_call_recording_url: {recording_url}
    hs_call_body: |
      CALL SUMMARY (Dialpad AI)
      ──────────────────────────
      {ai_summary}
      
      ACTION ITEMS:
      {ai_action_items formatted as bullet list}
      
      Sentiment: {sentiment}
      Duration: {duration_seconds} seconds
      Direction: {direction}
      Call ID: {call_id}
  Associations:
    Contact: {matched_contact_id}
    Company: {company_id from contact association}
  
  Update Contact properties:
    last_dialpad_call_date: {started_at date}
    last_dialpad_call_direction: {direction}

STEP 4 — DEDUPLICATION CHECK (prevent double-logging from native integration)
  → Check HubSpot call activities on contact created in last 60 seconds
  → IF native Dialpad integration already created a call activity 
     for this call_id: PATCH that record to add ai_summary body
     rather than creating a duplicate activity
  → IF no native activity exists: the Step 3 POST is the primary record

──────────────────────────────────────────────────────────────────

UNMATCHED CALL FLOW (Pass 4 — no contact match found)

  Review Queue Dashboard (Airtable):
  ┌──────────┬───────────┬────────────┬──────────┬──────────────┐
  │ Date     │ Number    │ Direction  │ Duration │ Rep          │
  ├──────────┼───────────┼────────────┼──────────┼──────────────┤
  │ Oct 14   │ +1312...  │ Inbound    │ 5m 42s   │ J. Smith     │
  │ Oct 14   │ +1708...  │ Outbound   │ 2m 11s   │ M. Johnson   │
  └──────────┴───────────┴────────────┴──────────┴──────────────┘
  
  Admin actions available per row:
  → "Create Contact" — opens HubSpot new contact form pre-filled
  → "Match to Existing" — search existing contacts, link call
  → "Dismiss" — marks as non-prospect (wrong number, vendor, etc.)

Dialpad AI Summary Push — Why Middleware Is Required

The Dialpad AI summary is the most valuable data in the call payload — and it is also the data point that Dialpad's native HubSpot integration does not push to the call activity body in all account and plan configurations. The native integration creates the call activity with duration, direction, and recording link. The AI summary, action items, and sentiment score are fields in the Dialpad webhook payload that require a custom webhook handler to parse and write to the HubSpot call activity body.

The Make.com middleware layer described above handles this explicitly: the call activity body is constructed as a formatted text block that includes the AI summary, action items as a bulleted list, and sentiment — all written as the hs_call_body property of the HubSpot call activity. This makes the AI summary searchable in HubSpot's activity timeline, readable by any team member without needing to open the Dialpad recording, and available as context for deal stage workflows that trigger on keyword presence in call notes.

•   •   •

The Matching and Reconciliation Layer — How Every Ambiguous Record Gets Handled

The matching layer is the architectural component that enforces the no-silent-drop requirement across all four integration pillars. It is not a single system — it is a set of matching hierarchies, confidence scoring rules, and review queue workflows that together ensure every piece of activity data either attaches to a correct record or surfaces for human review. The matching hierarchy is defined once, implemented consistently across all pipelines, and documented clearly enough that the admin maintaining the system after handoff can diagnose a matching failure without understanding the underlying API calls.

The Five-Level Match Hierarchy

Pass Match Key Confidence Action on Match Action on No Match
Pass 1 Phone in E.164 format (phone_e164 custom property) High (95+) Associate activity to matched contact. Update match_confidence_score = 95. Proceed to Pass 2.
Pass 2 Email address (exact match on HubSpot Contact email or additional email fields) High (95+) Associate activity. Update phone_e164 on contact if phone was the source record but email confirmed the match. Proceed to Pass 3.
Pass 3 BC Customer ID (bc_customer_id on Company object) High (90+) for company-level; medium (70) for contact-level association within company Associate activity to Company. If company has one associated Contact, associate to Contact as well. If multiple contacts, flag for rep to confirm which contact the activity belongs to. Proceed to Pass 4.
Pass 4 Fuzzy company name match (normalized: lowercase, remove legal suffixes Inc/LLC/Ltd/Corp, Levenshtein distance ≤ 2) Medium (50–75) — score varies by name similarity If confidence ≥ 70: create provisional association AND set match_review_flag = true. Record appears in review queue but is not left unassociated. If confidence < 70: send to review queue without provisional association. Proceed to Pass 5.
Pass 5 Email domain match (domain of inbound email matches domain of a HubSpot Company's website property) Low (40–60) — confirms company but not contact Associate activity to Company record. Flag Contact association for human confirmation. Set match_confidence_score = 50 and match_review_flag = true. No match at any level. Create review task. No silent drop.

The Review Queue — Non-Developer Administration

The review queue is the operational surface where the admin manages all records that did not match confidently through the five-pass hierarchy. The design principle for the review queue is identical to the design principle for the matching layer itself: zero silent drops, zero engineering dependency. The admin must be able to see every flagged record, understand why it was flagged, take action on it with a defined set of options, and clear the flag — all without touching a Make.com scenario or a HubSpot API call.

The review queue is implemented as a HubSpot Smart List combined with a dedicated view in each object type. The Smart List filter: match_review_flag = true. The view surfaces the record alongside its match_confidence_score and a note field populated by the matching scenario with the reason for the flag: "Fuzzy company name match — 68% confidence," or "Unmatched phone number from Dialpad call," or "Email domain match only — contact not confirmed."

Review Queue Item Type Flag Trigger Admin Actions Available Resolution Clears Flag
Low-confidence Company match Fuzzy name match < 70% confidence on BC Customer import Confirm match (correct company identified), Reject match (create new Company), Merge with existing (duplicate found) Yes — match_review_flag set to false by workflow on admin confirmation action
Unmatched Dialpad call Phone number not found in any HubSpot Contact after 5-pass hierarchy Create Contact (pre-filled with phone number), Match to existing Contact (search by name), Dismiss (non-prospect — mark number as excluded) Yes — call activity associated to contact on resolution; excluded numbers added to suppression list
Unmatched Outlook email Email from/to address not in HubSpot Contact database Create Contact (pre-filled with email address), Match to existing Contact, Dismiss (internal or irrelevant) Yes — email activity retroactively associated to contact on resolution
Teams meeting unlogged Rep did not confirm meeting association within 24 hours Log to Contact (confirm which contact), Log as internal (no CRM record needed), Dismiss with reason Yes — activity created on confirmation; task closed on dismissal
BC sync duplicate risk BC Customer No. found on two HubSpot Company records Merge Companies (select primary record), Confirm separate (different entities with same number — rare), Escalate to BC admin Yes — only after merge or explicit admin confirmation; never auto-resolved
•   •   •

Lead Capture and CSV Import Workflow — Tradeshows, Forms, and Source Tracking

Lead capture has two surfaces in this build: the website form (real-time, automated) and the tradeshow CSV import (batch, semi-automated with validation). Both surfaces must feed into the same matching hierarchy before any new record is created — to prevent duplicating contacts that already exist as BC customers in the HubSpot Company database.

Website Form Configuration

HubSpot's native form builder handles the web form with zero middleware required. The critical configuration is the hidden field set that captures lead source data automatically — the fields that tell the marketing and sales team where this lead came from, which campaign drove the conversion, and which page they converted on.

WEB FORM HIDDEN FIELD CONFIGURATION — LEAD SOURCE CAPTURE
FIELD 01
hs_analytics_source — HubSpot managed, auto-populated HubSpot automatically captures the traffic source (Organic Search, Direct, Social, Paid Search, Email, Referral) from the referring URL. No configuration required — this field populates natively on every form submission.
FIELD 02
utm_source / utm_medium / utm_campaign — Hidden, URL-populated Three hidden fields on every HubSpot form, mapped to UTM parameter values in the page URL. When a form submission arrives via a campaign link (email campaign, LinkedIn ad, Google Ads), the UTM values are captured automatically. These fields use HubSpot's "pre-fill from URL" feature — no JavaScript required.
FIELD 03
lead_source_detail — Custom property, form-specific values A custom Contact property with a defined dropdown: Website Form / Tradeshow — [Show Name] / CSV Import / Dialpad Inbound / Referral / BC Migration. Each form submission sets this to "Website Form." Each CSV import batch sets this to the relevant tradeshow name. Each Dialpad unmatched-to-new-contact flow sets this to "Dialpad Inbound." Consistent sourcing data across all lead entry points.

Tradeshow CSV Import — The Safe Import Workflow

Tradeshow lead lists arrive as Excel or CSV files, typically with inconsistent column naming, mixed phone formats, and company names that may or may not match existing BC customers in HubSpot. The import workflow for these lists is the same pre-validation architecture described in the HubSpot build rate post — but simplified here to a process a non-developer can execute using a documented template and a Make.com scenario that does the matching and validation automatically.

TRADESHOW CSV IMPORT WORKFLOW — NON-DEVELOPER EXECUTABLE
──────────────────────────────────────────────────────────────────

STEP 1 — STANDARDIZE TO IMPORT TEMPLATE (admin)
  Required template columns (provide to tradeshow lead collection app):
  FirstName | LastName | Email | Phone | CompanyName | JobTitle |
  LeadSource | ShowName | Notes

  Admin action: copy raw tradeshow export into template columns
  Time required: 15–30 minutes for typical 50–200 record list
  Common issues handled by template instructions:
    → Phone: include country code (template instructions: "+1 for US")
    → Company: full legal name preferred (inc/LLC not required to drop)
    → Email: lowercase, no spaces (template auto-formats on paste)

STEP 2 — UPLOAD TO MAKE.COM VALIDATION SCENARIO (admin)
  Admin uploads standardized CSV to a designated Google Drive folder
  → Make.com scenario triggers on file upload to folder
  → Scenario runs validation pass on each row:
      Email format valid? (RFC 5322 check)
      Phone normalizable to E.164?
      Company name present?
      Duplicate email in HubSpot? → FLAG, do not create
  
  → Validation report written to Google Sheet (admin-readable):
  ┌────────────────┬──────────────┬─────────────────────────────┐
  │ Row  │ Name   │ Status       │ Issue                       │
  ├──────┼────────┼──────────────┼─────────────────────────────┤
  │ 1    │ J.Lee  │ ✓ IMPORT     │ —                           │
  │ 2    │ S.Wang │ ⚠ REVIEW     │ Email already in HubSpot    │
  │ 3    │ M.Kim  │ ✗ SKIP       │ Missing email and phone     │
  │ 4    │ A.Cruz │ ✓ IMPORT     │ —                           │
  └──────┴────────┴──────────────┴─────────────────────────────┘

STEP 3 — ADMIN APPROVES IMPORT (review validation report)
  Admin reviews flagged rows:
    → REVIEW rows: decide merge vs. new contact vs. tag existing
    → SKIP rows: determine if fixable (add missing data) or exclude
  Admin marks approval in Google Sheet: "APPROVED" in column A
  Make.com monitors for approval → proceeds to Step 4

STEP 4 — MATCHED IMPORT EXECUTION (Make.com)
  For each IMPORT row:
    → Run 5-pass match hierarchy against HubSpot
    → IF high-confidence match (existing contact): UPDATE with
       tradeshow tag, show name, notes — do NOT create duplicate
    → IF no match: CREATE new Contact
       Set: lead_source_detail = "Tradeshow — {ShowName}"
       Set: lifecycle_stage = "Lead"
       Enrol in "New Lead — Tradeshow" workflow sequence
  
  Import summary report emailed to admin on completion:
  "Import complete: 142 contacts processed.
   94 new contacts created. 38 existing contacts updated.
   10 records skipped (see validation report rows 3, 7, 15...)"
•   •   •

HubSpot Tier Recommendation — Minimum Viable for This Scope

The minimum HubSpot tier required for this architecture is Sales Hub Professional, paired with the Connected Inbox feature which is available from the Starter tier upward. The Professional tier is not optional — three capabilities in this build require it: workflow automation beyond basic (the matching and review queue workflows use branching logic, delay steps, and custom object associations that are not available in Starter), custom object creation (the Sales Order custom object for BC Sales Orders requires Professional or above), and the deal-based workflow automation that drives deal stage changes from BC Quote status updates.

Capability Required Minimum Tier Why This Tier — Not Lower
Connected Inbox (Outlook auto-logging) Sales Hub Starter Available from Starter — no tier constraint on this feature.
Custom Properties (bc_customer_id, phone_e164, etc.) Free CRM Custom properties are available on the free tier. No upgrade required for property creation alone.
Workflow Automation (multi-branch, custom object triggers) Sales Hub Professional Starter workflows are limited to basic contact-based triggers with no branching. The matching review queue, Dialpad call logging, and BC sync notification workflows require Professional-tier workflow capabilities.
Custom Objects (Sales Order object for BC Orders) Sales Hub Professional Custom objects are a Professional-tier feature. Cannot be created on Starter or Free. Required for BC Sales Order association architecture.
Deal Pipeline Automation (stage changes from BC status) Sales Hub Professional Deal-based workflow triggers and deal stage automation require Professional. Starter allows manual stage movement only.
Smart Lists (review queue contact lists) Marketing Hub Starter (if using marketing lists) or Sales Hub Professional Active lists that auto-update based on criteria (match_review_flag = true) are available in Marketing Hub Starter. Sales Hub Professional includes list functionality through workflow targeting. Confirm with HubSpot account representative which list type is needed for the specific review queue Smart List configuration.

The practical recommendation: start with Sales Hub Professional for the seats actively using the CRM (sales reps and admin). Marketing Hub Starter can be added if email campaigns and form analytics beyond basic are required. Service Hub is not needed for this scope. The total seat cost at Professional tier for a team of 5–10 sales users is the primary cost variable — the integration infrastructure (Make.com, Microsoft Graph API access, Dialpad webhooks) does not add HubSpot tier cost.

•   •   •

Admin Guide Principles — What Non-Developer Maintainability Actually Requires

The handoff to a non-developer admin is not a documentation deliverable. It is an architectural constraint that must be enforced during the build, not described afterward. A system that requires a developer to diagnose a sync failure is a system that was not built with the handoff constraint in mind — regardless of how good the admin guide documentation is.

The specific architectural decisions that enforce non-developer maintainability in this build: Make.com is chosen over custom serverless functions specifically because Make.com's scenario execution history shows every step, every data value, and every error in a visual interface that an admin can read without understanding JSON. The review queue is implemented as a HubSpot Smart List with a dedicated view — not as a separate database or admin panel — because the admin already operates in HubSpot daily and does not need to learn a new interface. The validation report for CSV imports is written to Google Sheets — not a custom web application — because every admin in a 60-year-old company knows how to read a Google Sheet.

Every integration component in this build has a corresponding admin guide section that answers three questions: what does this look like when it is working correctly, what does it look like when it is failing, and what are the three most common causes of failure and how do you fix each one without calling a developer. That structure — working state, failure state, three common fixes — is more valuable than a comprehensive technical reference manual that an admin will never read when something breaks at 9am.

"The real test of a handoff-ready integration is not whether the admin guide exists. It is whether an admin who has never seen the system before can diagnose a sync failure in under 10 minutes using only the tools they already have access to. If that is not true, the architecture is wrong — not the documentation." Arsalan Faysal — Revenue Systems Architect
•   •   •

What This Architecture Delivers — The Operational State After Build

When this architecture is fully deployed and stable, the operational reality for the sales team is this: a rep picks up the phone, takes a Dialpad call, and the call duration, direction, recording link, and AI-generated summary appear on the contact's HubSpot timeline automatically — no logging, no copy-paste, no navigation to the CRM during the call. An account manager sends an email from Outlook and it appears on the contact record within minutes — no add-in required, no manual log action. A sales coordinator returns from a tradeshow with a 150-row Excel file, uploads it to a Google Drive folder, receives a validation report, approves the import, and 142 clean contact records appear in HubSpot with correct source attribution — no developer involved. The Business Central team closes a Sales Order and the sales rep sees updated order value and order history on the HubSpot Company record the next time they open it — no manual sync, no data entry.

Every ambiguous piece of data — an unrecognized phone number from a Dialpad call, an email from an address not yet in the CRM, a BC customer name that fuzzy-matches two HubSpot companies — surfaces in a review queue that the admin clears each morning. Not a silent drop. Not a data quality incident discovered six months later during an audit. A flagged item in a list, with the information needed to make a decision, and the ability to take that decision without engineering support.

That is the system this architecture builds. The 60 years of customer relationships that this company has earned deserve a CRM infrastructure that treats every customer touchpoint as worth capturing and worth getting right. The automation is not a convenience feature. It is the organizational commitment that every interaction is recorded, every ambiguity is surfaced, and every data point that matters to the relationship is in the right place for the next person who talks to that customer.

In This Article Object Schema First Custom Property Architecture Deals vs. Custom Objects Pillar 1 — Business Central Connector Selection Field Protection Layer Pillar 2 — Outlook Connected Inbox Config Pillar 3 — Teams Phase 1 Meeting Workflow Pillar 4 — Dialpad Webhook Architecture AI Summary Push Matching & Reconciliation 5-Level Match Hierarchy Review Queue Design Lead Capture & CSV Import HubSpot Tier Recommendation Admin Guide Principles
Stack Built in This Architecture
HubSpot Sales Pro CRM & Relationship Hub
Microsoft Business Central ERP Source of Record
Dialpad Call Intelligence Layer
Microsoft Outlook Email Auto-Logging
Microsoft Teams Meeting Recording Link
Make.com No-Code Middleware Layer
BC OData API ERP Data Extraction
MS Graph API Teams Event Webhooks