Skip to content

02 Get your data in /Integrations

Fathom

Send Fathom meeting transcripts to Dimension Labs with the universal API.

  1. F FathomTickets, chats, calls, reviews
  2. ⟳ Your jobPull new · map fields · every 15 min
  3. { } Universal APIPOST /track · /trackhistorical
  4. Dimension LabsRead in full · enriched daily

This brings in each Fathom meeting transcript as one conversation, turn by turn: what your prospects and customers said, and what your own team said. You run it on your side: a small script pulls from the Fathom API and posts each turn to the universal API.

  • A Fathom user whose API key can see the meetings you want. API access is included on all Fathom plans. Keys are per user, and there are no org-level keys: for org-wide access, give an Admin view access to all shared calls and use that Admin’s key. Private meetings stay visible only to their host’s own key.
  • Optionally, the email domains your company uses, for example acme.com, as a fallback for telling your team apart.
  • A Dimension Labs API key for a new Universal API source. See Your API key.
  1. In Fathom, open User Settings and go to API Access (quickstart).
  2. Generate an API key and copy it.
  3. For org-wide access, have an account admin give that user view access to all shared calls, as described in Fathom’s FAQ.

Send the key on every request in the X-Api-Key header.

List meetings created in a time window, with their transcripts:

Terminal window
curl -G -H "X-Api-Key: $FATHOM_API_KEY" "https://api.fathom.ai/external/v1/meetings" \
-d include_transcript=true \
-d created_after=2026-10-25T00:00:00Z \
-d created_before=2026-10-26T00:00:00Z
  • created_after and created_before filter on the meeting’s created_at.
  • Each page returns a next_cursor. Send it back as cursor until it is null.
  • Each meeting has a recording_id, a recording_start_time and, with include_transcript=true, a transcript: a list of items, each with a speaker (display_name, and matched_calendar_invitee_email when Fathom matched the speaker to a calendar invitee), the text, and a timestamp (HH:MM:SS from the start of the meeting).
  • calendar_invitees lists each invitee’s email, email_domain and is_external.
  • Without recorded_by[] or teams[], meetings recorded by users outside your Fathom org aren’t returned.
  • The code waits FATHOM_DELAY_HOURS (1 by default) before it reads a meeting, so its transcript is ready. Fathom can also notify you by webhook when new meeting content is ready.

Rate limits, per Fathom: 60 calls in a 60 second window. Requests with include_transcript=true are heavy requests: 30 in 60 seconds, which Fathom may lower to 5 during busy periods. Going over returns 429; wait at least Retry-After seconds (API overview).

Dimension LabsFathomNotes
texttranscript[].textOne message per transcript item.
userIda hash of speaker.matched_calendar_invitee_email, else of display_nameNever the email or name itself. A speaker with no matched email gets a hash of the recording id and display name.
incomingthe matched invitee’s is_externaltrue for external invitees (prospects, customers), false for your team. With no matched invitee, the code treats the person who recorded the meeting, and emails on your COMPANY_DOMAINS, as your team; anyone else is incoming.
sessionIdrecording_idPrefixed, for example fathom-123456789.
conversationIdrecording_idSame value as sessionId.
dimensionlabs_timestamprecording_start_time plus timestamptimestamp is to the second, so items in the same second are spaced to 1,000 ms.
platformJsonmeeting_type, transcript_language, calendar_invitees_domains_type; speakerMatched, internalcalendar_invitees_domains_type says whether the invitee list included external domains.

Fathom tells your team apart itself: each calendar invitee has is_external, and each transcript speaker carries the email of the invitee Fathom matched them to.

Personal data. Speaker names and invitee emails are used only to match and hash, and are never sent. Leave out title, meeting_title and crm_matches, which name the customer. Transcripts hold names, companies and numbers: redact them before you send. See PII redaction.

// fathom.mjs. Environment: FATHOM_API_KEY, COMPANY_DOMAINS (optional, for example "acme.com,acme.co.uk"), FATHOM_DELAY_HOURS (default 1), DL_API_KEY.
import { createHash } from 'node:crypto';
import { sendToDimensionLabs, toMillis } from './dimensionlabs.mjs';
const HOUR = 60 * 60 * 1000;
const API = 'https://api.fathom.ai/external/v1';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const DELAY = Number(process.env.FATHOM_DELAY_HOURS ?? 1) * HOUR;
const DOMAINS = (process.env.COMPANY_DOMAINS ?? '').toLowerCase().split(',').map((d) => d.trim()).filter(Boolean);
const hash = (s) => createHash('sha256').update(s.trim().toLowerCase()).digest('hex').slice(0, 32);
const seconds = (hms) => hms.split(':').reduce((a, p) => a * 60 + Number(p), 0);
// Retries Fathom's rate limits (429) and server errors (5xx), honoring Retry-After.
async function get(url) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, { headers: { 'X-Api-Key': process.env.FATHOM_API_KEY } });
if (res.ok) return res.json();
if ((res.status !== 429 && res.status < 500) || attempt === 5) throw new Error(`Fathom returned ${res.status}: ${await res.text()}`);
await sleep((Number(res.headers.get('retry-after')) || 2 ** attempt) * 1000);
}
}
// Sends every meeting created at or after `since` (Unix ms) and at least FATHOM_DELAY_HOURS ago. Returns the new `since`.
export async function syncFathom({ since, historical = false }) {
const until = Date.now() - DELAY;
if (until <= since) return since;
const params = { include_transcript: 'true', created_after: new Date(since).toISOString(), created_before: new Date(until).toISOString() };
for (let cursor = null; ; ) {
const page = await get(`${API}/meetings?${new URLSearchParams({ ...params, ...(cursor ? { cursor } : {}) })}`);
for (const m of page.items ?? []) {
const invitees = new Map((m.calendar_invitees ?? []).filter((i) => i.email).map((i) => [i.email.toLowerCase(), i]));
const recorder = m.recorded_by?.name?.trim().toLowerCase();
const session = `fathom-${m.recording_id}`;
const start = toMillis(m.recording_start_time);
let last = 0;
for (const item of m.transcript ?? []) {
if (!item.text?.trim()) continue;
const email = item.speaker?.matched_calendar_invitee_email?.toLowerCase();
const name = item.speaker?.display_name?.trim();
const invitee = email && invitees.get(email);
const internal = invitee ? !invitee.is_external
: Boolean((name && name.toLowerCase() === recorder) || (email && DOMAINS.includes(email.split('@')[1])));
const ts = Math.max(start + seconds(item.timestamp) * 1000, last + 1000); // keep items at least a second apart
last = ts;
await sendToDimensionLabs({
text: item.text,
userId: email ? `fathom-${hash(email)}` : name ? `fathom-${hash(`${m.recording_id}-${name}`)}` : session,
incoming: !internal,
sessionId: session,
conversationId: session,
dimensionlabs_timestamp: ts,
platformJson: {
meetingType: m.meeting_type, language: m.transcript_language, inviteeDomains: m.calendar_invitees_domains_type,
speakerMatched: Boolean(email), internal,
},
}, { historical: historical || ts < Date.now() - HOUR });
}
}
cursor = page.next_cursor;
if (!cursor) return until;
}
}

Save the code as fathom.mjs in a project set up as in Set up your project, and run it with the scheduled sync runner: import { syncFathom as sync } from './fathom.mjs';.

  • History first: node run.mjs 2025-01-01 sends every meeting since that date to /trackhistorical. Then start a backfill from the Backfill tab.
  • Then every 15 minutes: the scheduled job sends meetings once they are FATHOM_DELAY_HOURS old. By then the turns are more than an hour old, so they go to /trackhistorical: start a backfill whenever you want the latest meetings enriched.
  • Re-runs are safe: duplicates are removed automatically. If a transcript was late, run node run.mjs with a date before the meeting to pick it up.

Every page with transcripts is a heavy request, so a history load is held to Fathom’s heavy-request limit; the code waits whenever Fathom asks it to. You can also upload a file of exported transcripts (see Call transcripts, and check it with the file checker), then run a backfill.

Checked against Fathom’s documentation on 2026-10-26.