Intercom
What the Intercom connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/intercom.py. Kind: both (claims and facts). Display name: "Intercom (Fin)".
Purpose
Pulls Fin AI Agent outcomes as claims (one resolution claim per conversation Fin marks as resolved) and the conversation record as facts (customer messages, human replies, assignments, closes/reopens, conversation ratings) so that the intercom_fin_resolution and intercom_fin_procedure_handoff contracts can be evaluated.
Auth and scopes
| Item | Value |
|---|---|
| Method | Access token (Intercom OAuth app token or workspace access token), sent as Authorization: Bearer; API version header Intercom-Version: 2.11 |
| Credentials stored (encrypted) | access_token; optional base_url (defaults to https://api.intercom.io) |
| Least-privilege scopes | Read conversations, Read contacts, Read admins, Read conversation ratings. No write scopes. |
| Connection test | GET /me (workspace name is recorded in health) |
| Non-secret config | contract_id (default intercom_fin_resolution), unit_price (default 0.99) |
Fields pulled and why
| Field | Used for |
|---|---|
conversation.id, created_at, updated_at |
Subject id, opening message timestamp, incremental watermark |
conversation.state |
(read; not currently stored) |
conversation.source.type, source.delivered_as |
Channel (email when the source type is email, otherwise delivered_as or chat) for channel exclusions |
conversation.ai_agent.resolution_state, resolved_at, last_answer_type |
Whether Fin counted a resolution (claim), and its timestamp |
conversation_parts[].part_type, author.type, assigned_to.type, created_at, id |
Fact classification: customer message vs human reply vs bot message, escalation, close, reopen |
conversation_rating.rating, created_at |
CSAT fact (1–5 scale) |
contacts.contacts[0].email |
HMAC-hashed into customer_hid for cross-source identity resolution |
| Assignee (via assignment parts) | Escalation detection |
Conversations are fetched with display_as=plaintext when parts are missing from the search result, so that part authorship can be read; message bodies are not stored.
What is hashed / never stored
- Contact email →
HMAC-SHA256with the tenant key →attributes.customer_hid. The email itself is discarded. - Message bodies, contact names, phone numbers, custom attributes, tags, and any other conversation content are not persisted.
- The access token is stored only as AES-256-GCM ciphertext.
Incremental sync and watermark
POST /conversations/searchwithupdated_at > <watermark as epoch seconds>(0 on first sync),per_page: 150, cursor pagination viapages.next.starting_after.- Each conversation is fully ingested on every appearance; upserts are idempotent (
claim_id/fact_idderived fromsha256(tenant_id | raw_ref)), so re-syncs update rather than duplicate. - Watermark = the maximum
updated_atseen, stored asconnections.watermark; the next sync starts from it. A manual backfill (POST /v1/connections/{id}/sync {"backfill_days": N}, max 400) temporarily moves the start back. The initial sync after credentials are saved backfills 90 days.
Rate-limit handling
Shared HttpClient: on 429 or any 5xx it waits Retry-After if present, otherwise min(60, 2^attempt) seconds, up to 6 attempts, then fails the sync with gave up after retries. 401/403 fail immediately with reauth = true (see RUNBOOKS.md → Connector auth failure). Request count is recorded in health.requests.
Fact types produced
Subject kind: conversation. Every fact carries customer_hid and channel in attributes.
fact_type |
When | Extra attributes | raw_ref |
|---|---|---|---|
customer_message |
Conversation source message; any comment part authored by user, lead or contact |
part: "source" for the opener |
intercom:conversation:{id}:source, intercom:part:{id}:{part_id} |
human_reply |
comment part authored by admin |
author: "admin" |
intercom:part:… |
agent_message |
comment part authored by bot |
author: "fin" |
intercom:part:… |
escalation |
assignment / assign_and_reopen part assigning to an admin |
to: "human" |
intercom:part:… |
status_change |
close part → `from: |
bot | agent; open/reopen/assign_and_reopenpart →from: closed, to: open, actor: |
reopen |
open / reopen / assign_and_reopen part |
by: <author type> |
intercom:part:… |
csat_response |
conversation_rating.rating present |
score (1–5), scale: 5, sentiment (≥4 positive, ≤2 negative, else neutral) |
intercom:rating:{id} |
customer_confirmation |
ai_agent.resolution_state == confirmed_resolution |
method: "fin_confirmed" |
intercom:fin_confirmation:{id} |
Claims produced
| Condition | Claim |
|---|---|
ai_agent.resolution_state in confirmed_resolution, assumed_resolution, resolved |
outcome_type: resolution, claimed_at = ai_agent.resolved_at (falls back to updated_at), unit_price from config, channel, attributes: {resolution_state, customer_hid}, raw_ref: intercom:fin_outcome:{id}, contract_id from config |
Subject id format
intercom_conv_{conversation_id} for both claims and facts.
Known limitations
- Only resolution claims are produced. Procedure-handoff outcomes (
intercom_fin_procedure_handoff) must currently be supplied via CSV upload. conversation_rating.created_atmay be absent; the connector then uses the conversation'supdated_atas the rating timestamp, which can be later than the actual rating.- The connector does not store message text, so transcript assist cannot use Intercom conversations unless the tenant also uploads a CSV with
keep_text. - Assignment to a team (not an admin) is not treated as an escalation.
- Fin's own "returned for further assistance" deduction is not read from Intercom; reopen facts are evaluated by the contract instead.
- Voice channels are not distinguished beyond
source.delivered_as; the library contract excludesvoice/phoneby channel name.