We built a production Fathom to HubSpot integration that listens to Fathom webhooks, parses AI summaries and action items, and creates routed HubSpot tasks and a linked call note automatically. It is for sales and success teams that want next steps logged in the CRM without manual data entry. This guide shows the exact architecture, code patterns, and the gotchas we hit.
Fathom to HubSpot automation is: using Fathom's webhooks and public API to convert meeting data into structured HubSpot tasks and notes so follow-ups land in the pipeline without manual logging.
The problem it solves
Most teams leave next steps in meeting notes or email threads. Someone has to remember to open the CRM, create a task, set due dates, and paste the context. That manual hop is why action items fall through the cracks and why managers see incomplete timelines in HubSpot.
| Manual workflow | Automated with Fathom to HubSpot |
|---|---|
| Reps copy action items from Fathom and hand-create tasks in HubSpot after each call | Tasks are created instantly when Fathom posts a webhook. Due dates, owners, and links to the recording are set by rules |
| Notes pasted inconsistently into the CRM | Clean call note stored with summary, key moments, and a link back to Fathom |
| Backfilling past calls requires hours of copy-paste | One backfill job pulls past meetings from the Fathom API and seeds tasks |
| High variance in what gets logged | Deterministic mapping rules turn action items into consistent task titles and properties |
According to Asana's Anatomy of Work 2023, knowledge workers spend an estimated 58 percent of their time on work about work like searching, switching tools, and manual status updates. Source: https://asana.com/resources/anatomy-of-work-2023
How the automation works
The system has four pieces: a Fathom webhook sender, a small server that validates and maps payloads, the HubSpot task and note creators, and an optional backfill job that uses the Fathom API.
- Fathom webhooks: Fathom sends meeting payloads that can include AI summary, transcript, and action items. We register our endpoint in Fathom settings and include a secret in the URL so only our server processes legitimate traffic.
- Orchestrator: A lightweight server parses the payload, normalizes action items, computes owners and due dates, and deduplicates by meeting plus action hash.
- HubSpot writers: One function creates a CRM task and another records a call note that links to the Fathom recording. Authentication uses the client's standard server-side method for HubSpot.
- Backfill via API: For older meetings, we pull from Fathom's public API at https://api.fathom.ai/external/v1 with an X-Api-Key and replay the same mapping to seed tasks.
Step-by-step: how to build it
1) Register the Fathom webhook and include a shared secret
Create a webhook in Fathom settings that fires on meeting completion. Append a secret token in the URL and reject requests that do not include the token. Fathom documents first-party webhooks and public API access with X-Api-Key.
# Example webhook target we register inside Fathom settings
https://your-domain.example/api/fathom/webhook?token=${FATHOM_WEBHOOK_TOKEN}Key point: do not assume payloads are trustworthy. Gate everything behind your own secret and log IPs for incident reviews.
2) Parse the payload and normalize action items
We accept JSON, validate the token, and map action items into a deterministic structure that later becomes HubSpot tasks. We stamp a stable dedupe key using the meeting id and action text.
// api/fathom/webhook.js
import crypto from "node:crypto";
export default async function handler(req, res) {
const token = req.query.token;
if (token !== process.env.FATHOM_WEBHOOK_TOKEN) return res.status(401).end();
const body = req.body; // Express/Next body parser enabled
const meetingId = body?.meeting?.id;
const summary = body?.ai_summary || "";
const recordingUrl = body?.recording_url || "";
const actions = Array.isArray(body?.action_items) ? body.action_items : [];
const normalized = actions.map((a, idx) => {
const title = a.title?.trim() || a.text?.trim() || "Follow up";
const hash = crypto
.createHash("sha1")
.update(`${meetingId}:${title}`)
.digest("hex")
.slice(0, 16);
return {
key: `${meetingId}:${hash}`,
title,
// simple rule: due in 2 business days unless a date was detected
dueDate: pickDueDate(a?.due_date, 2),
ownerHint: inferOwnerFromParticipants(body?.participants),
context: { meetingId, summary, recordingUrl }
};
});
// enqueue for writing to HubSpot
await queueTasks(normalized);
return res.status(202).json({ accepted: normalized.length });
}
function pickDueDate(raw, fallbackBusinessDays) {
if (raw) return new Date(raw).toISOString();
const d = new Date();
let added = 0;
while (added < fallbackBusinessDays) {
d.setDate(d.getDate() + 1);
const day = d.getDay();
if (day !== 0 && day !== 6) added++;
}
return d.toISOString();
}
function inferOwnerFromParticipants(participants = []) {
// simple heuristic: if exactly one teammate joined, assign to them
const teammates = participants.filter(p => p?.is_internal === true);
return teammates.length === 1 ? teammates[0].email : null;
}Gotcha: do not trust free-text due dates in action items. Always coerce to ISO and fall back to a business-days rule.
3) Create the HubSpot task safely and idempotently
We hide HubSpot specifics behind a helper so the mapping is testable and idempotent. The helper looks for an existing task with our dedupe key and creates one only if none exists.
// lib/hubspot.js
export async function createCrmTask(task) {
// task: { key, title, dueDate, ownerHint, context }
if (await existsByKey(task.key)) return { status: "skipped", reason: "duplicate" };
const payload = {
title: task.title,
dueAt: task.dueDate,
ownerEmail: task.ownerHint,
properties: {
source: "fathom",
meeting_id: task.context.meetingId
}
};
await hubspotPostTask(payload); // wraps HubSpot auth and request
await rememberKey(task.key);
return { status: "created" };
}Key point: idempotency belongs to you. Compute and persist a dedupe key so retries cannot create duplicates.
4) Attach a call note with the AI summary and recording link
We also write a call note so the task has context. Keep the note concise and include a link back to Fathom.
// lib/hubspot-notes.js
export async function attachCallNote(contactId, context) {
const note = [
`Call summary:`,
context.summary?.slice(0, 1200),
"",
`Recording: ${context.recordingUrl}`
].join("\n");
await hubspotPostNote({ contactId, body: note });
}Gotcha: avoid pasting full transcripts. Use the AI summary and keep the link to the source of truth.
5) Backfill older meetings with the Fathom API
For initial seeding or recoveries, pull meetings from the Fathom API and replay the same mapping. Auth uses an X-Api-Key header as documented.
// scripts/backfill-fathom.js
import fetch from "node-fetch";
const BASE = "https://api.fathom.ai/external/v1";
async function listMeetings(sinceIso) {
const resp = await fetch(`${BASE}/meetings?since=${encodeURIComponent(sinceIso)}`, {
headers: { "X-Api-Key": process.env.FATHOM_API_KEY }
});
if (!resp.ok) throw new Error(`Fathom list failed ${resp.status}`);
return resp.json();
}
(async () => {
const data = await listMeetings(process.env.BACKFILL_SINCE_ISO);
for (const m of data?.meetings || []) {
const actions = m.action_items || [];
for (const a of actions) {
// build the same normalized task object as the webhook path
// then call createCrmTask(normalized)
}
}
})();Note: for large backfills, page through results and throttle your writes. Fathom recommends the public API for retroactive automation.
6) Store a small ledger for diagnostics and retries
Keep a lightweight table to record every write and its outcome. It makes retries and audits simple.
-- postgres table
create table if not exists fathom_task_ledger (
key text primary key,
meeting_id text not null,
created_at timestamptz default now(),
status text not null,
error text
);Gotcha: do not couple retries to your incoming webhook thread. Put tasks on a queue and have a worker perform CRM writes.
7) Test against real calls, then enable for the team
Run two weeks in shadow first: write tasks to a test pipeline or with a test label and compare to human entries. Turn on for the first team once dedupe, routing, and due dates look right.
Where it gets complicated
- Email matching in Fathom: Fathom matches external contacts by primary email only. Secondary emails are not matched. If a contact uses an alias, your HubSpot association may miss without a fallback match strategy.
- Internal calls do not sync: The native sync is designed for external calls. Internal only meetings are out of scope. Plan your routing rules accordingly.
- Calendar source constraints: Sync works for scheduled calls on your main calendar. Secondary calendars are not picked up, which can explain missing webhooks for some users.
- Disconnects and historical gaps: If Fathom disconnects from HubSpot, future calls stop syncing. Historical edits require manual re-sync per call. Keep a health check that alerts on disconnect so you can avoid gaps.
- Retroactive automation at scale: Zapier style backfills are reviewed and limited. For bulk or ongoing exports, use Fathom's public API. We use the same mapper for webhooks and backfill to stay consistent.
- CRM write safety: Treat HubSpot writes as non-idempotent unless you add your own key. We compute a meeting plus action hash and skip when present to prevent duplicates on retries.
What this actually changes
In production this removed the after-call admin step. Reps stopped hand-creating tasks and managers saw complete timelines in HubSpot without chasing screenshots. The value is structural: every external call that Fathom hears becomes a task with a due date and a link to the recording. That reduces context loss and removes a class of misses that only show up at quarter end.
Industry context: Asana's Anatomy of Work 2023 estimates knowledge workers spend 58 percent of their time on work about work like status updates and tool switching. Automating call follow-up removes a slice of that overhead. Source: https://asana.com/resources/anatomy-of-work-2023
Frequently asked questions
Does Fathom have an API for this?
Yes. Fathom offers a public API and first-party webhooks. The API uses an X-Api-Key header and the documented base URL is https://api.fathom.ai/external/v1. Webhooks can include summaries, transcripts, and action items.
Can I do this with Zapier or Make instead of code?
Yes. Fathom has native Zapier and Make apps with triggers for new AI summaries and recordings. For simple flows they work well. For large backfills or stricter dedupe and routing rules, we prefer the public API plus a small service.
What about internal meetings or secondary calendars?
Fathom's HubSpot sync scope is external calls on your main calendar. Internal only calls are not synced and secondary calendars are not picked up. If you need coverage beyond that, run the webhook plus API approach and apply your own routing rules.
How do you prevent duplicate tasks in HubSpot?
We compute a stable key from the Fathom meeting id and the action text, store it in a small ledger, and skip writes when the key exists. Retries and backfills use the same dedupe so the pipeline stays clean.
What does this cost each month?
The server and API costs are minimal for most teams. The main cost is the initial build. If you run this through Zapier or Make you will pay per task. A small custom service keeps unit costs close to zero and gives you better control over dedupe and backfills.
If you want this wired into your pipeline without guesswork, we have already shipped it. See our related walkthrough on how we automate Fathom meeting notes, our broader CRM automation services, and when you are ready to scope your version, book a 15-minute call.
Want us to build this for you?
15-minute discovery call. No pitch. We tell you what to automate first.
Book a Discovery Call