02 Get your data in /Integrations
Fathom
Send Fathom meeting transcripts to Dimension Labs with the universal API.
- F FathomTickets, chats, calls, reviews
- ⟳ Your jobPull new · map fields · every 15 min
- { } Universal APIPOST /track · /trackhistorical
- 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.
What you need
Section titled “What you need”- 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. Get API access in Fathom
Section titled “1. Get API access in Fathom”- In Fathom, open User Settings and go to API Access (quickstart).
- Generate an API key and copy it.
- 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.
2. Pull new meetings
Section titled “2. Pull new meetings”List meetings created in a time window, with their transcripts:
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:00Zcreated_afterandcreated_beforefilter on the meeting’screated_at.- Each page returns a
next_cursor. Send it back ascursoruntil it isnull. - Each meeting has a
recording_id, arecording_start_timeand, withinclude_transcript=true, atranscript: a list of items, each with aspeaker(display_name, andmatched_calendar_invitee_emailwhen Fathom matched the speaker to a calendar invitee), thetext, and atimestamp(HH:MM:SSfrom the start of the meeting). calendar_inviteeslists each invitee’semail,email_domainandis_external.- Without
recorded_by[]orteams[], 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).
3. Map the fields
Section titled “3. Map the fields”| Dimension Labs | Fathom | Notes |
|---|---|---|
text | transcript[].text | One message per transcript item. |
userId | a hash of speaker.matched_calendar_invitee_email, else of display_name | Never the email or name itself. A speaker with no matched email gets a hash of the recording id and display name. |
incoming | the matched invitee’s is_external | true 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. |
sessionId | recording_id | Prefixed, for example fathom-123456789. |
conversationId | recording_id | Same value as sessionId. |
dimensionlabs_timestamp | recording_start_time plus timestamp | timestamp is to the second, so items in the same second are spaced to 1,000 ms. |
platformJson | meeting_type, transcript_language, calendar_invitees_domains_type; speakerMatched, internal | calendar_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.
4. Send it
Section titled “4. Send it”// 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; }}5. Run it
Section titled “5. Run it”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-01sends 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_HOURSold. 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.mjswith 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.
Fathom reference
Section titled “Fathom reference”- API overview (authentication, rate limits)
- Quickstart
- FAQ (API keys and org-wide access)
- Webhooks
- OpenAPI specification
Checked against Fathom’s documentation on 2026-10-26.