Blog

Syncing Apollo Intent Data to Salesforce Dashboards

Ankit Dhiman

Min Read

AEs live in Salesforce, not Apollo. Here's the exact API architecture to pipe real-time intent signals into custom CRM views — with threshold-based alerting.


Intent Data Your AEs Will Never See Is Just an Expensive Subscription

The RevOps leader who bought Apollo Pro justified it with intent data. The pitch was clear: know which accounts are actively researching your category and get there first. The data is real. The signal is genuinely useful. The problem is where it lives.

It lives in Apollo. Your AEs live in Salesforce. Between those two tabs is a behavior gap that kills the ROI of every intent platform in the market.

Nobody in a revenue organization consistently checks a second tool for context before working a record in their primary CRM. That's not a discipline problem — it's a design problem. The workflow is: open Salesforce, find the account, make the call or send the email. The Apollo tab is open somewhere in the browser. It gets checked when someone remembers, which means it gets checked sporadically by the people who care about process and almost never by the people who are heads-down on quota.

Intent data that lives outside the CRM is intent data that gets ignored. The fix isn't training the team to check more tabs. The fix is building the plumbing that brings the signal into the system where decisions actually get made.

This post covers the exact API architecture for syncing Apollo's intent data programmatically into Salesforce Account objects, building custom list views and dashboard components that surface intent context inline — and implementing threshold-based alerting so AEs only get notified when multiple signals cross a meaningful threshold simultaneously, not on every minor activity blip.

Why Native Apollo-Salesforce Sync Isn't Enough

Apollo has a native Salesforce integration. It syncs contacts, sequences, and activity data. What it does not do natively — at least not with the granularity and configurability most revenue teams actually need — is map intent topic data to Account-level fields in a way that drives CRM-native alerting and list view filtering.

The native integration is built for contact and activity sync. It will push a contact's Apollo profile and sequence status into Salesforce. It will not, by default:

  • Create custom Account fields populated with intent topic scores and timestamps

  • Trigger Salesforce-native Flow automations based on intent threshold crossings

  • Populate a custom "Intent Intelligence" related list on the Account page layout showing topic-level signal history

  • Drive Lightning dashboard components that rank accounts by current intent score without requiring a rep to leave Salesforce

For those capabilities, you need a custom sync architecture running in n8n that treats Apollo as a data source and Salesforce as the display and alerting layer.

The Architecture: Four Layers

Layer 1: Pulling Intent Data from Apollo's API

Apollo's Intent API (GET /api/v1/accounts/intent) returns account-level intent data including topic categories, signal strength, and trend direction. The endpoint requires an Apollo API key with Intent access (available on Apollo's Pro and custom enterprise plans).

The n8n workflow triggers on a scheduled basis every 6 hours — not real-time, because Apollo's intent data updates at the platform level on a cadence that doesn't warrant per-minute polling. Configure the HTTP Request node:


textGET https://api.apollo.io/api/v1/accounts/intent
Headers: {
  "Content-Type": "application/json",
  "Cache-Control": "no-cache",
  "X-Api-Key": "{{$env.APOLLO_API_KEY}}"
}
Query Parameters: {
  "organization_ids[]": "{{accountBatch}}",
  "intent_topics[]": ["crm automation", "revenue operations", "workflow orchestration", "sales automation"],
  "signal_strength[]": ["medium", "high", "very_high"]
}
textGET https://api.apollo.io/api/v1/accounts/intent
Headers: {
  "Content-Type": "application/json",
  "Cache-Control": "no-cache",
  "X-Api-Key": "{{$env.APOLLO_API_KEY}}"
}
Query Parameters: {
  "organization_ids[]": "{{accountBatch}}",
  "intent_topics[]": ["crm automation", "revenue operations", "workflow orchestration", "sales automation"],
  "signal_strength[]": ["medium", "high", "very_high"]
}
textGET https://api.apollo.io/api/v1/accounts/intent
Headers: {
  "Content-Type": "application/json",
  "Cache-Control": "no-cache",
  "X-Api-Key": "{{$env.APOLLO_API_KEY}}"
}
Query Parameters: {
  "organization_ids[]": "{{accountBatch}}",
  "intent_topics[]": ["crm automation", "revenue operations", "workflow orchestration", "sales automation"],
  "signal_strength[]": ["medium", "high", "very_high"]
}

The intent_topics[] filter is critical — you don't want all intent data Apollo tracks, only the topics relevant to your product category. Filtering at the API level reduces payload size and eliminates noise from unrelated research signals that would pollute your Salesforce fields.

The response structure returns per-account intent records:


json{
  "accounts": [
    {
      "id": "apollo_acc_8472",
      "domain": "scaleops.io",
      "intent_data": {
        "topics": [
          {
            "topic": "crm automation",
            "signal_strength": "very_high",
            "trend": "increasing",
            "last_signal_date": "2026-04-19T14:00:00Z",
            "weekly_volume_score": 87
          },
          {
            "topic": "revenue operations",
            "signal_strength": "high",
            "trend": "stable",
            "last_signal_date": "2026-04-18T09:00:00Z",
            "weekly_volume_score": 61
          }
        ]
      }
    }
  ]
}
json{
  "accounts": [
    {
      "id": "apollo_acc_8472",
      "domain": "scaleops.io",
      "intent_data": {
        "topics": [
          {
            "topic": "crm automation",
            "signal_strength": "very_high",
            "trend": "increasing",
            "last_signal_date": "2026-04-19T14:00:00Z",
            "weekly_volume_score": 87
          },
          {
            "topic": "revenue operations",
            "signal_strength": "high",
            "trend": "stable",
            "last_signal_date": "2026-04-18T09:00:00Z",
            "weekly_volume_score": 61
          }
        ]
      }
    }
  ]
}
json{
  "accounts": [
    {
      "id": "apollo_acc_8472",
      "domain": "scaleops.io",
      "intent_data": {
        "topics": [
          {
            "topic": "crm automation",
            "signal_strength": "very_high",
            "trend": "increasing",
            "last_signal_date": "2026-04-19T14:00:00Z",
            "weekly_volume_score": 87
          },
          {
            "topic": "revenue operations",
            "signal_strength": "high",
            "trend": "stable",
            "last_signal_date": "2026-04-18T09:00:00Z",
            "weekly_volume_score": 61
          }
        ]
      }
    }
  ]
}

Paginate through all accounts in your ICP list using Apollo's page and per_page parameters. Store the raw response in a PostgreSQL staging table before processing — this gives you a queryable history of intent signal changes over time, which matters for trend analysis and for the threshold logic in Layer 3.

Layer 2: Mapping Apollo Accounts to Salesforce Records

The Apollo account ID and your Salesforce Account ID are different systems with no native shared key. The mapping layer matches on domain — both Apollo and Salesforce store the company website domain, which is the most reliable non-proprietary identifier across tools.

A Salesforce HTTP Request node queries your Account records:


sqlSELECT Id, Name, Website, Apollo_Account_ID__c,
       Intent_Score__c, Intent_Topics__c,
       Last_Intent_Update__c
FROM Account
WHERE Website != null
  AND IsActive__c = true
sqlSELECT Id, Name, Website, Apollo_Account_ID__c,
       Intent_Score__c, Intent_Topics__c,
       Last_Intent_Update__c
FROM Account
WHERE Website != null
  AND IsActive__c = true
sqlSELECT Id, Name, Website, Apollo_Account_ID__c,
       Intent_Score__c, Intent_Topics__c,
       Last_Intent_Update__c
FROM Account
WHERE Website != null
  AND IsActive__c = true

A Function node then builds a domain-to-Salesforce-ID lookup map and joins it against the Apollo response:


javascriptconst sfAccounts = items[0].json.records;
const domainMap = {};

sfAccounts.forEach(acc => {
  const domain = acc.Website
    ?.replace(/^https?:\/\//, '')
    ?.replace(/^www\./, '')
    ?.split('/')[0]
    ?.toLowerCase();
  if (domain) domainMap[domain] = acc.Id;
});

// Join with Apollo intent data from previous node
const intentData = items[1].json.accounts;
const matched = intentData
  .filter(a => domainMap[a.domain])
  .map(a => ({
    salesforce_id: domainMap[a.domain],
    apollo_id: a.id,
    domain: a.domain,
    intent_topics: a.intent_data.topics
  }));

return matched.map(m => ({ json: m }));
javascriptconst sfAccounts = items[0].json.records;
const domainMap = {};

sfAccounts.forEach(acc => {
  const domain = acc.Website
    ?.replace(/^https?:\/\//, '')
    ?.replace(/^www\./, '')
    ?.split('/')[0]
    ?.toLowerCase();
  if (domain) domainMap[domain] = acc.Id;
});

// Join with Apollo intent data from previous node
const intentData = items[1].json.accounts;
const matched = intentData
  .filter(a => domainMap[a.domain])
  .map(a => ({
    salesforce_id: domainMap[a.domain],
    apollo_id: a.id,
    domain: a.domain,
    intent_topics: a.intent_data.topics
  }));

return matched.map(m => ({ json: m }));
javascriptconst sfAccounts = items[0].json.records;
const domainMap = {};

sfAccounts.forEach(acc => {
  const domain = acc.Website
    ?.replace(/^https?:\/\//, '')
    ?.replace(/^www\./, '')
    ?.split('/')[0]
    ?.toLowerCase();
  if (domain) domainMap[domain] = acc.Id;
});

// Join with Apollo intent data from previous node
const intentData = items[1].json.accounts;
const matched = intentData
  .filter(a => domainMap[a.domain])
  .map(a => ({
    salesforce_id: domainMap[a.domain],
    apollo_id: a.id,
    domain: a.domain,
    intent_topics: a.intent_data.topics
  }));

return matched.map(m => ({ json: m }));

Unmatched domains — Apollo accounts with no corresponding Salesforce record — get written to a separate PostgreSQL table for RevOps review. These are often accounts you should have in Salesforce but don't, which is its own data hygiene signal.

Layer 3: Threshold Calculation and Composite Scoring

This is the layer that separates a useful intent integration from a noisy one. The goal is not to alert AEs on every intent signal — it's to alert them only when multiple signals cross a defined threshold simultaneously, indicating an account has entered an active in-market window rather than just brushing past a topic once.

The composite intent score combines three variables per account:


javascriptfunction calculateCompositeIntentScore(topics) {
  const strengthWeights = {
    "very_high": 40,
    "high": 25,
    "medium": 10
  };

  const trendMultipliers = {
    "increasing": 1.4,
    "stable": 1.0,
    "decreasing": 0.6
  };

  let compositeScore = 0;
  let activeTopicCount = 0;

  topics.forEach(topic => {
    const basePoints = strengthWeights[topic.signal_strength] || 0;
    const trendFactor = trendMultipliers[topic.trend] || 1.0;

    // Recency decay — signals older than 7 days lose weight
    const daysSinceSignal = Math.floor(
      (Date.now() - new Date(topic.last_signal_date)) / 86400000
    );
    const recencyFactor = Math.max(0.3, 1 - (daysSinceSignal / 14));

    compositeScore += basePoints * trendFactor * recencyFactor;

    if (topic.signal_strength !== "low") activeTopicCount++;
  });

  return {
    composite_score: Math.round(compositeScore),
    active_topic_count: activeTopicCount,
    threshold_crossed: compositeScore >= 65 && activeTopicCount >= 2
  };
}
javascriptfunction calculateCompositeIntentScore(topics) {
  const strengthWeights = {
    "very_high": 40,
    "high": 25,
    "medium": 10
  };

  const trendMultipliers = {
    "increasing": 1.4,
    "stable": 1.0,
    "decreasing": 0.6
  };

  let compositeScore = 0;
  let activeTopicCount = 0;

  topics.forEach(topic => {
    const basePoints = strengthWeights[topic.signal_strength] || 0;
    const trendFactor = trendMultipliers[topic.trend] || 1.0;

    // Recency decay — signals older than 7 days lose weight
    const daysSinceSignal = Math.floor(
      (Date.now() - new Date(topic.last_signal_date)) / 86400000
    );
    const recencyFactor = Math.max(0.3, 1 - (daysSinceSignal / 14));

    compositeScore += basePoints * trendFactor * recencyFactor;

    if (topic.signal_strength !== "low") activeTopicCount++;
  });

  return {
    composite_score: Math.round(compositeScore),
    active_topic_count: activeTopicCount,
    threshold_crossed: compositeScore >= 65 && activeTopicCount >= 2
  };
}
javascriptfunction calculateCompositeIntentScore(topics) {
  const strengthWeights = {
    "very_high": 40,
    "high": 25,
    "medium": 10
  };

  const trendMultipliers = {
    "increasing": 1.4,
    "stable": 1.0,
    "decreasing": 0.6
  };

  let compositeScore = 0;
  let activeTopicCount = 0;

  topics.forEach(topic => {
    const basePoints = strengthWeights[topic.signal_strength] || 0;
    const trendFactor = trendMultipliers[topic.trend] || 1.0;

    // Recency decay — signals older than 7 days lose weight
    const daysSinceSignal = Math.floor(
      (Date.now() - new Date(topic.last_signal_date)) / 86400000
    );
    const recencyFactor = Math.max(0.3, 1 - (daysSinceSignal / 14));

    compositeScore += basePoints * trendFactor * recencyFactor;

    if (topic.signal_strength !== "low") activeTopicCount++;
  });

  return {
    composite_score: Math.round(compositeScore),
    active_topic_count: activeTopicCount,
    threshold_crossed: compositeScore >= 65 && activeTopicCount >= 2
  };
}

The threshold_crossed boolean requires both a composite score of 65+ and at least 2 active intent topics simultaneously. A single very-high-strength topic on one day could be a one-off research session. Two topics crossing medium-or-higher strength in the same week indicates a buying committee that's actively researching multiple dimensions of the problem — a much stronger in-market signal.

Layer 4: Writing Intent Data to Salesforce Custom Fields

For each matched account where the calculation has run, the workflow fires a Salesforce PATCH to update custom Account fields:


jsonPATCH /services/data/v59.0/sobjects/Account/{{salesforce_id}}
{
  "Apollo_Intent_Score__c": 78,
  "Apollo_Intent_Tier__c": "High",
  "Apollo_Active_Topics__c": "crm automation; revenue operations",
  "Apollo_Topic_Count__c": 2,
  "Apollo_Trend__c": "Increasing",
  "Apollo_Last_Signal_Date__c": "2026-04-19",
  "Apollo_Threshold_Crossed__c": true,
  "Apollo_Last_Sync__c": "2026-04-20T04:00:00Z"
}
jsonPATCH /services/data/v59.0/sobjects/Account/{{salesforce_id}}
{
  "Apollo_Intent_Score__c": 78,
  "Apollo_Intent_Tier__c": "High",
  "Apollo_Active_Topics__c": "crm automation; revenue operations",
  "Apollo_Topic_Count__c": 2,
  "Apollo_Trend__c": "Increasing",
  "Apollo_Last_Signal_Date__c": "2026-04-19",
  "Apollo_Threshold_Crossed__c": true,
  "Apollo_Last_Sync__c": "2026-04-20T04:00:00Z"
}
jsonPATCH /services/data/v59.0/sobjects/Account/{{salesforce_id}}
{
  "Apollo_Intent_Score__c": 78,
  "Apollo_Intent_Tier__c": "High",
  "Apollo_Active_Topics__c": "crm automation; revenue operations",
  "Apollo_Topic_Count__c": 2,
  "Apollo_Trend__c": "Increasing",
  "Apollo_Last_Signal_Date__c": "2026-04-19",
  "Apollo_Threshold_Crossed__c": true,
  "Apollo_Last_Sync__c": "2026-04-20T04:00:00Z"
}

The Apollo_Intent_Tier__c picklist field — None / Low / Medium / High / Very High — is the field that drives your Salesforce List Views. AEs can open their "High Intent Accounts" list view and see every account currently crossing the threshold, sorted by Apollo_Intent_Score__c descending, without leaving Salesforce or thinking about Apollo at all.

The Salesforce-Side Configuration That Makes It Actionable

Pushing data into Salesforce fields is only half the work. The fields need to surface in the right places to change rep behavior.

Custom Account List View — "Intent Signals: Active"
Filter: Apollo_Threshold_Crossed__c = true AND Apollo_Last_Signal_Date__c = LAST_7_DAYS
Columns: Account Name | AE Owner | Intent Score | Active Topics | Topic Trend | Last Signal Date | Last Activity Date

This view becomes the daily prospecting queue for proactive outbound. Accounts appearing here are actively researching — they've self-identified without filling out a form.

Account Page Layout — Intent Intelligence Panel
Add a custom Lightning component (a simple Visualforce section works for non-technical orgs) that renders the Apollo_Active_Topics__c field as styled topic badges and shows the Apollo_Intent_Score__c as a progress bar. When an AE opens any Account record, intent context is visible inline — no tab switching, no separate lookup.

Salesforce Flow — Threshold Alert Notification
Build a Record-Triggered Flow on Account that fires when Apollo_Threshold_Crossed__c changes from false to true:

  • Send an in-app Salesforce notification to the Account Owner: "[Account Name] just crossed intent threshold — 2 topics active, score 78. View account →"

  • Create a High Priority Task due today: "Intent threshold crossed — reach out while signal is active"

  • If OwnerId is the Marketing Queue (unassigned account): ping the #unassigned-intent-alerts Slack channel via a connected Salesforce Flow action

The Slack alert for unassigned accounts is the one that generates the most immediate pipeline impact. High-intent accounts with no AE owner are your hottest prospecting targets — they're actively researching and nobody on your team is currently working them.

Intent Data That Lives in Salesforce Gets Used

The behavioral reality of a B2B sales team is simple: if a signal doesn't appear in the tool where reps do their work, it doesn't change their behavior. Apollo's intent data is genuinely valuable — it surfaces accounts in active buying windows that your outbound motion would otherwise approach cold. But that value is entirely contingent on it being visible at the moment a rep is deciding who to call.

Custom Salesforce fields, list views, and threshold-based alerting built on top of the Apollo API turns intent data from a tab you theoretically have open to a native part of the workflow where decisions get made. The intelligence was always there. This architecture just puts it where it gets seen.

Your Revenue Stack Shouldn't Require Tab Management

If your AEs are expected to maintain context across Apollo, Salesforce, 6sense, LinkedIn Sales Navigator, and whatever intent tool is newest in the stack — you don't have a revenue intelligence system. You have a collection of subscriptions that compete for attention.

Chronexa architects revenue stack integrations that collapse fragmented signal sources into a single CRM-native workflow. The engagement starts with a stack audit — we map every tool in your current revenue motion, identify the data each one generates, and design the API architecture that surfaces the right signals in Salesforce at the right moment without requiring reps to manage the complexity themselves.

If your team is paying for intent data that AEs aren't consistently acting on, that audit will show you exactly why — and what the integration needs to look like to change it.

Book the revenue stack integration session here

— bring your current tool list and we'll design the unified architecture your AEs will actually use.

About author

Ankit is the brains behind bold business roadmaps. He loves turning “half-baked” ideas into fully baked success stories (preferably with extra sprinkles). When he’s not sketching growth plans, you’ll find him trying out quirky coffee shops or quoting lines from 90s sitcoms.

Ankit Dhiman

Head of Strategy

Subscribe to our newsletter

Sign up to get the most recent blog articles in your email every week.

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:02:33 AM

Chronexa

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:02:33 AM

Chronexa

Sometimes the hardest part is reaching out, but once you do, we’ll make the rest easy.

Opening Hours

Mon to Sat: 9.00am - 8.30pm

Sun: Closed

2:02:33 AM

Chronexa