The pattern
1
Backfill once
Page
/calls?has_summary=true, /decision-rooms (then /decision-rooms/{id}/metrics for each), and /engagement/events from an empty cursor until hasMore is false. Store each feed’s nextCursor.2
Poll incrementally
Every few minutes, call each feed with its stored cursor and upsert whatever comes back on
id. Store the new cursor.3
React to webhooks
Verify the signature, dedupe on the envelope
id, apply the payload, then schedule an incremental pass a few seconds out. The webhook gets you latency; the cursor gets you completeness.Entity mapping
Attribution when no room is linked
Engagement and calls arrive before a rep links the room to a deal. Two rules that hold up:- Store it anyway. An event you cannot place yet is history you cannot get back later. Keep it keyed by
buyerSpaceIdand attach it the moment the link is made. - Fall back to a person on exactly one open deal. If an event’s
actor.emailmatches a contact who sits on precisely one open deal, that attribution is safe. If they sit on two, refuse to guess — a confidently wrong attribution makes a dead deal look alive, which is the failure the engagement data exists to prevent.
Idempotency
Every path can be hit twice for the same fact: TrailerCast retries webhooks, and your poll overlaps your webhook handler by design. Dedupe before aggregating, not after — watch-time totals inflate silently if a retry is counted twice, and nothing will look wrong.Things not to do
- Do not treat
lastActivityAton a room as a buyer signal on its own. It moves on seller edits too. Use/decision-rooms/{id}/metrics→totals.lastActivityAt, which is buyer-only. - Do not create contacts for every participant. Filter on
isInternal === falseand require an email; a name-only participant cannot be deduplicated. - Do not poll
/decision-rooms/{id}/metricsfor every room on every tick. Fetch metrics for rooms that appeared in the latest/decision-roomspage — those are the ones that changed.
