Hub / Existing Solutions / Fin · Intercom

Fin / Intercom

A gateway-class connector: the integration runs as a service outside Intercom, because Intercom will not run member code inside the workspace. TSANet can host that service for you, or you can run it in your own cloud environment; the adapter is the same either way. This page is the architecture as built, and the platform behaviour an implementer needs to know before starting.

Class Gateway adapter Status Early access Surface Canvas Kit sidebar, Help Desk Hosting TSANet-hosted or self-hosted Last verified 2026-10-20 Updated 2026-10-28 Repo tsanetgit/Fin-Intercom_App (private during early access)

Connector class

Why This Connector Is Shaped Differently

Every connector TSANet shipped before this one assumes code can run inside the member's own system. Salesforce and Dynamics allow it; Zendesk partly; ServiceNow would, though nothing is built there yet. Intercom does not — and neither will most of the platforms behind it. That single constraint means the integration has to run as a service somewhere outside the platform, and someone has to operate it: TSANet, or the member in its own environment. Which one is the member's choice; hosting options compares the two.

Zendesk sits between the two: its apps run client-side in a sandbox and its integration flows are Zendesk-hosted, so some logic has a home in-tenant but arbitrary server code does not.
 In-tenant connectorGateway adapter
PlatformsSalesforce, Dynamics (ServiceNow would fit here; not built)Intercom / Fin, Pylon, DevRev, Wolken
Where code runsInside the member's own tenantA service outside the platform, hosted by TSANet or in the member's own cloud
Who operates itThe memberWhoever hosts it: TSANet, or the member
Credentials liveIn the member's tenantWith whoever hosts the service, one set per member
Case lifecycle stateThe CRM holds itThe gateway must hold it
Connector faultsIsolated to one memberCorrelated across the members on a TSANet-hosted service; one member's own when self-hosted. A single replica in the reference deployment either way
Platform outageAffects everyone either way — the Connect API is a shared dependency
DistributionInstalled package or appConnected app plus a service, TSANet-hosted or self-hosted

The two failure rows are worth reading together. Every member already depends on the Connect API, so a platform outage stops collaboration whichever class they are in — an in-tenant package does not keep working when the platform is down. What a gateway changes is that connector faults become correlated rather than independent, which is a design input rather than a drawback: it is why the gateway is designed to a higher availability target than any single member's install — and, with the reference deployment a single replica today, why that target is met through durability and recovery rather than replication, and why so much of the design below is about exactly that rather than features. A member that hosts its own deployment shares a connector fault with no one, and takes on the operating work in exchange.

Three Differences That Force the Change

  1. 01

    There is nowhere in the member's system to put connector code

    Intercom offers an API, webhooks, sign-in, and panels rendered from your own servers. There is no equivalent of an installed package running inside the customer's instance, so everything runs on infrastructure outside the platform: TSANet's, or the member's own.

  2. 02

    The unit of work is a conversation, not a record

    Other connectors assume a case record with a rich status model, custom fields, and a platform-maintained audit trail. Intercom's native object is a conversation thread; tickets were added later and carry only a handful of states. State other connectors get for free has to be kept by the gateway instead.

  3. 03

    The AI answers first, and a human is the escalation path

    Fin is designed to resolve before a person is involved, and Intercom charges per resolution rather than per seat. A collaboration request arriving into that world is expected to be answered, not queued — and the TSANet lifecycle assumes a human picks the case up.

What the Lifecycle Actually Permits

The open design question was whether an incoming partner request could be answered and closed out without ever becoming a ticket — the flow Intercom is built around. Probed against a live environment rather than reasoned from documentation:

partner submits a request 200 status OPEN, response clock starts
we answer with a note 200 allowed without accepting
partner tries to close it 422 "OPEN cases cannot be closed."
we try to close it 422 same refusal — not a permissions issue

Half of it works. Answering requires no change to the case's status at all, so an AI agent can do its entire job inside a conversation, with nothing written back but the answer. But no case can be retired while it is still open, by either party. Every request that ends cleanly must first be accepted.

The permitted lifecycle for a collaboration request A request moves from Open to Accepted to Closed. While it is Open, a conversation loop runs on the Open state: partner notes come in and Fin's answers go out, over any number of rounds, without changing the status. A direct transition from Open to Closed is refused with HTTP 422. the conversation loop — any number of rounds notes in, Fin's answer out · status stays OPEN OPEN ACCEPTED CLOSED accept close 422 — refused for both parties
The refused edge is the constraint the design has to accommodate: acceptance is not optional, so it cannot be treated as the moment work becomes real. Incoming requests land as a conversation and become a ticket only when a person needs to own the work — tying ticket creation to acceptance would create a ticket for every single request, which is precisely what the conversation-first model exists to prevent.
Recommended guardrail: the AI drafts, a person commits. Fin composes the answer and posts it. Accepting, declining, and requesting more information stay with a human. The reasoning is not answer quality — it is that these actions are contractual commitments to another vendor made under a service-level clock, and that partner escalations carry end-customer logs and text pasted in verbatim: untrusted content reaching an AI that then replies under the member's name.

Hosting

Two Ways to Run It: TSANet-Hosted or Self-Hosted

The adapter is one container image with one configuration surface, and it runs the same way wherever it is deployed. A member can have TSANet host and operate it, or run it in the member's own cloud account. TSANet supports both, and the choice is the member's. The deciding questions are usually about policy rather than engineering: where case content and credentials may live, who is on call for the service, and who holds encryption keys once end-to-end encryption applies.

 TSANet-hostedSelf-hosted
Who runs itTSANet deploys, monitors and upgrades the serviceYou deploy, monitor and upgrade it, in your own cloud account
CredentialsYour Connect API credential and Intercom app secrets are held by TSANetThey never leave your environment
Stored case dataThe link store and the SDK cache (which holds case content) sit on TSANet's infrastructureBoth stay on yours, under your retention and backup policy
Connector faultsShared with the other members on the same serviceYours alone
UpgradesTSANet rolls out each adapter releaseYou build and deploy each release from its tag, after reading its release notes; you choose the window
MonitoringTSANet, including its internal admin consoleYou. The admin console is not connected to a self-hosted deployment unless you and TSANet agree to connect it
What you set upThe same either way: the Intercom private app, ticket type and ticket-state ids, and a Connect API credential per environment

What a Self-Hosted Deployment Needs

These come from how the engine and the adapter work, so they apply on any cloud and any container service.

Where It Has Actually Run

PlatformState
Azure Container Appsreference The deployment TSANet runs. A spec template and deploy script ship with the adapter: an app container plus a backup sidecar, the registry pulled by managed identity.
AWSuntested No AWS deployment exists and TSANet has not run one. A service shaped like the Azure one (one always-running task, public HTTPS ingress, a sidecar for the backup loop, secrets from AWS Secrets Manager) meets the requirements above. Plan a full Beta validation before treating it as ready.
Any other container platformuntested The same container contract applies. Nothing about the adapter is Azure-specific; the backup and restore steps are plain shell over two mounted paths.

Getting the Image, and Upgrading

No image is published for members to pull today. You build it from the adapter's release tag with the repository's Dockerfile, which needs JDK 21, Maven and Docker. The build resolves the engine and connect-library from TSANet's GitHub package registry, which requires a GitHub token even for reads; the engine package is private, so registry access is granted at onboarding along with the step-by-step self-hosting guide. A ready-to-pull published image is a known gap.

Every upgrade repeats the same short loop: read the release notes (they carry an "Upgrading a deployment" section), compare the deploy spec between your tag and the new one, build and push the image, confirm your backups are current, deploy in a quiet window, then verify the running image is the one you built. Rolling back is the same loop with the previous tag.

Moving From TSANet-Hosted to Self-Hosted

Agree a cutover window with TSANet first. TSANet stops its deployment and hands over the final copies of both stores, taken after the stop rather than from the last periodic backup; you place them where your restore step reads them and only then start yours. Never run the two side by side: the reconciliation poller alone, with no webhook at all, is enough to double-create tickets.

End-to-end encryption: tracked, not built. In Connect's end-to-end encryption design, protected case fields are encrypted and signed at the member's own connector, and the platform holds no keys. For a gateway adapter, that puts the keys wherever the adapter runs, which makes self-hosting the mode in which a member holds its own keys. Neither the engine nor this adapter implements it yet: today the adapter sends the encryption fields empty on every create. Do not plan an encrypted deployment on this adapter until it ships; it is listed under known gaps. How the design works, and where the other connectors stand, is on the end-to-end encryption topic.

Topology

The System at a Glance

One headless Spring Boot process sits between the two platforms. Everything TSANet-facing lives in an embedded gateway-engine shared across adapters — the Connect Gateway, which this connector was the proving ground for and now consumes as a published artifact — gateway-engine 3.0.0 with connect-library 3.0.0 since 2026-10-20, the two pins moved together by the build; everything Intercom-facing goes through a REST gateway. The process border is the trust boundary: signature verification is default-deny, so a route matching no scheme is rejected 401, and a missing secret fails closed. /healthz is the only open route; the TSANet-internal admin plane under /admin/* is a fourth signed edge, under CONNECTOR_ADMIN_API_SECRET.

Intercom Help Desk (agent) "TSANet Connect" sidebar panel Canvas Kit · ticket + conversation markdown links + bold only, no HTML agents never see the service itself Intercom platform canvas POSTs + webhook delivery weak contract: 5s timeout, 1 retry, 429 throttling → fast-ack + queue Intercom REST API tickets · admin notes · ticket states returns silent 200s on bad fields: every write is read back connector process · Spring Boot · single replica border = signature verification · default-deny HMAC · /healthz only open route Canvas flow CanvasController · CanvasKit FlowStateStore · bounded + TTL stored_data carries {flow_id} only ≤1 gateway call per canvas response gateway-engine (embedded) SdkTsanetGateway MemberSessionRegistry generated Connect client deadlines · sanitization · sessions shared across adapters Webhook intake IntercomWebhookController TsanetWebhookController · v2 only DedupeStore · LRU 10,000 ids always 200: 4xx would kill retry EventQueue bounded · offer() drops on full one drain thread, deliberately Event handlers InboundRequestHandler claim-before-create InboundClosureHandler platform first, then record PartnerResponseHandler once per status transition AnnotationHandler NONE/DEGRADED/FULL · 5s/20s ×3 ReconciliationPoller re-drives what push dropped updatedAt cursor − 120s overlap advances only on a clean cycle Durable state SubmissionLinkTable · never evicted keys: token · internalCaseNo · conv PollerStateStore · same store durable volume required RestIntercomGateway annotate · resolve · note, from the drain only, never a canvas response TSANet Connect API · v1/v2 entity search · process form createCollaborationRequest approve · reject · request-info closure · list (internalCaseNo) OAuth (Entra) or password login no idempotency key on create problem+json via Accept opt-in prod: connect2.tsanet.org Connect webhook push CloudEvents v2 · v1 sunset 2027-01 request.created · request.closed note.created · response.created response.updated at-least-once · may duplicate Partner member works the same case from their own connector or the web app clicks · input 1 2 3 4 5 search · form · submit ↴ dedupe → queue drain fetchByToken · approve/reject/RFI record · anchor · surfaced status listInboundUpdatedSince handle() direct · bypasses the queue cursor → case events
The runtime as built. Accent edges cross the signed process boundary and are numbered in the legend below; plain edges are in-process calls. The green store is the only state that survives a restart.

Flow 1 · outbound

An Agent Files a Case with a Partner

The canvas flow keeps all real state server-side (stored_data carries an opaque {flow_id} only), makes at most one gateway call per canvas response, and never retries in-handler: the canvas latency budget is hard — an 8s response renders, a 10s one fails, and a slow submit strands the agent on a spinner with no error at all. The flow id doubles as the submission's internalCaseNumber, one per flow by construction.

Panel Connector gateway-engine Connect API /v1 Intercom API POST /canvas/initialize · signed · ticket key rides every ticket-surface request home canvas · stored_data = {flow_id} /canvas/submit · search "q" searchPartners(q) · deadline 1500ms, no retries entity search · bearer per request (registry session) List<PartnerHit> partner list canvas select partner form(partner) · fresh per flow, never cached fetch process form form canvas · STRING/EMAIL/PHONE/INTEGER/URL→input · TEXT→textarea · HIDDEN unrendered submitter identity prefilled from acting teammate, editable submit form (typed values win over defaults) flow state DRAFT → SUBMITTING (CAS) · flow_id = internalCaseNumber submit(draft) · deadline 4000ms total createCollaborationRequest · testSubmission per config (default true) 200 + case token (no idempotency, no conflict rejection) record SubmissionLink durably · state → SUBMITTED success canvas async, on the drain: annotate ticket attributes, read back · NONE/DEGRADED/FULL · retries 5s/20s ×3
Happy path. Dashed arrows are responses. The annotation write never happens inside a canvas response — the latency budget forbids Intercom calls there; it rides the event queue instead. Attributes are keyed by name, datetimes take epoch seconds, and a workspace-side rename surfaces as the read-back failing, not as a write error.

Flow 1a · submit outcomes

What a Submit Can Become, and Why Ambiguity Is Parked

Case submission is not idempotent and the Connect API offers no idempotency key: createCollaborationRequest documents 200/400/401/403 and no conflict status, so a second POST with the same internalCaseNumber simply creates a second case. The gateway contract therefore splits failures into exactly two kinds, and treats everything ambiguous as ambiguous.

submit(draft) ≤ 4000ms total 200 + token Created record SubmissionLink → SUBMITTED annotation event queued for the ticket parsed 400 · 401 · 403 · 404 · 422 GatewayException · definitive rejected before create → back to DRAFT problem+json detail rendered next to the field that fixes it (email-domain rule) timeout · 5xx · reset · token-less 200 GatewayTimeoutException · ambiguous flow parked UNRESOLVED · exempt from store eviction and TTL (severing one orphans the case) reconcile findSubmission(flow_id) best-effort lookup, NOT an idempotency guarantee found → SUBMITTED case confirmed none → DRAFT re-armed >1 match → throws (no silent confirm)
The UNRESOLVED → DRAFT transition on an empty lookup is the server-side marker that re-arms retry; component ids from the client are never trusted for it. An empty lookup cannot prove the original request is not still in flight, so the residual duplicate window is accepted and named in the agent-facing copy. The same taxonomy gates the inbound response actions — a 5xx on an approve read as "definitively rejected" would invite answering a partner twice.

Flow 2 · inbound

A Partner Opens a Case with Us

Push is thin and delivered at-least-once, so intake fast-acks and the fetch is the truth: the event contributes only the token. Ticket creation is claim-before-create, arbitrated by the token's UNIQUE constraint in the link table, because check-then-create is a TOCTOU race that has produced duplicate-ticket defects in an earlier connector (note.created overtaking collaboration-request.created).

Partner TSANet Connect Connector Intercom API Agent (panel) opens a collaboration case naming our member CloudEvent collaboration-request.created · signed · thin payload (token; CloudEvent id = only idempotency key) verify → v2 parse (unroutable refused, still 200) → dedupe → queue 200 fast-ack, every branch (4xx is never retried by TSANet) drain: fetchByToken(token) · the fetch is the truth direction INBOUND? (UNKNOWN never guessed) · terminal?CLOSED/REJECTED → skip, never create-resolved record() claim · token UNIQUE arbitrates · only the insertwinner creates · failed create releases claim (unless anchored) create Back-office ticket · configured type · synthetic partner contact id (API handle) + ticket_id (agent-searchable number) caseNumber = ticket_id, captured at create · anchor() write-once panel renders from recorded CaseState: Accept · Decline · Request information Accept · engineer identity prefilled, editable (registered-domain rule) CAS state first: two open panels cannot both answer approve(token, engineer, caseNumber, nextSteps) · via engine, ≤4000ms · 5xx = ambiguous, state stays moved partner sees the acceptance + our searchable case number later: collaboration-request.closed → resolve ticket FIRST, then record CLOSED (a failed resolve records nothing, so redelivery can fix it) response.created / .updated (our outbound cases) → fetch status → surface once per transition: ticket state + admin note
Inbound create and answer. Inbound links carry inbound:<token> as their internalCaseNumber, a local provisional convention. An unset ticket type refuses to create; a blank anchor id is refused in both stores so the release guards agree by construction. One ticket can anchor several cases, so byTicketId refuses a multiple match.

Flow 3 · recovery

Re-Driving What Push Dropped

Between an at-least-once push that can still be lost and a drain that catches internally, anything lost in the middle used to be lost permanently. The reconciliation poller closes that gap: it lists INBOUND cases via listInboundUpdatedSince(cursor − 120s) and drives anything with no link back through the same claim-before-create path.

  • Cursoring on updatedAt was verified, not assumed: a live-created case came back with updatedAt == createdAt to the nanosecond, and a filter set 60s earlier returned it. A naive reading of the data suggested updatedAt only moved on modification, which would have missed new cases for 24h.
  • It calls the handler directly, not through the event queue: a recovery batch offered to a bounded queue could evict live webhook events. The claim arbitrates between the poller thread and the drain exactly as between two deliveries.
  • Success means an anchored link, not a row existing: an unanchored row reads as in-flight and holds the cursor.
  • The cursor advances only after a fully clean cycle, and only to an updatedAt actually observed. An empty window advances nothing. A wedged cursor degrades to a widening query and loud logs, never lost work.
  • Terminal cases are prefiltered (still advancing the cursor), so a missed create plus a partner close cannot manufacture a dead pending ticket. The prefilter runs before the claim lookup, so an orphaned claim on a terminal case cannot wedge the cursor.
  • Orphaned claims are surfaced, not reclaimed: age-based reclaim risks pulling a claim from under a live-but-slow create, recreating the duplicate-ticket class. Releasing one stays a human action.
  • The cursor is persisted before the cycle is declared healthy, in the same durable store as the links — so a cursor cannot outlive the links it cursors over.
  • Bounded windows: the list endpoint has no pagination, so the window is the only load control; the first run asks for now − 24h, never epoch. Single instance only.

What Survives a Restart, and What Deliberately Does Not

StoreSurvives restartNotes
SubmissionLinkTableyes · requiredThe token ↔ ticket routing substrate; indexed by token, internalCaseNumber and conversation id; never evicted. Carries CaseState, annotation state, case number and surfaced status.
PollerStateStoreyesReconciliation cursor. A lost cursor costs one wider re-scan; lost links orphan every open case — which is why only the cursor is allowed an in-memory fallback.
connect-sdk.db (SDK cache)yesThe embedded engine's operational cache: case summaries, note text, response details, webhook receipts. A cache, not the source of truth — the panel and the poller re-fetch by token — which is what makes the optional retention sweep safe: CONNECTOR_SDK_CACHE_RETENTION (default off, per deployment) evicts terminal cases and their child rows past the window; OPEN cases never age out. Backed up with the link store (5-minute latest copy plus hourly snapshots on a 48-hour window), so evicted content is gone from every store at most 48h after the sweep.
FlowStateStoreno · by designCanvas flow state. SUBMITTING and UNRESOLVED flows are exempt from eviction and TTL; an expired flow restarts cleanly from the home canvas.
DedupeStorenoIn-memory LRU, 10,000 ids. Replay past the bound is re-processed; accepted because downstream handling is idempotent anyway.
EventQueuenoBounded; offer() drops on full. Both annotation offer sites audit the failure with the token so the manual re-drive path holds.
MemberSessionRegistrynoPer-member session, gateway and API client pairing, with a credential fingerprint and rebuild-on-change. The API client is stateful; sharing one across members would bleed credentials.
Deployment answer, probed rather than assumed. SQLite write-ahead logging cannot run on a network-filesystem mount — probed live on Azure Files SMB, where it fails loudly at the very first connection. The reference deployment therefore keeps the database on replica-local storage and pairs it with a backup loop to durable storage (copied every few minutes, restored on boot), and the production profile still refuses to start on a memory store, a relative path or a fake gateway — and a deployment missing SPRING_PROFILES_ACTIVE=prod gets none of those refusals and no signal that it is running on sand. A backup loop is not a database: worst case a restart loses the last few minutes of link writes, which is acceptable for a pilot and not for production — production replaces the file with a managed database, by design a configuration swap rather than a rewrite.

Vocabulary

Connect Case Status → Intercom Ticket State

Connect statusIntercom ticket stateWhy
OPENSubmittedThe partner asked; nobody here has answered yet.
ACCEPTEDIn progressWe took it.
INFORMATIONWaiting on customerBlocked on the partner. Intercom's external label reads "Waiting on you" — cosmetic on Back-office tickets today.
REJECTEDResolvedTerminal. Ticket state alone cannot distinguish declined from finished; the distinction must live on a custom attribute.
CLOSEDResolvedTerminal.
PENDINGACTIONnever movedThinly documented — a bare enum member in the spec; the API page reads it as a case awaiting an action from your side. What this adapter should do with it is an open question.
UNKNOWNnever movedAn unrecognised wire value by construction. A stale state is visibly wrong; a guessed one is invisibly wrong.

Platform notes

What an Implementer Needs to Know About Intercom

Measured against a free Intercom development workspace (US region) and the TSANet BETA environment. Development workspaces are free, do not expire, and the Tickets API is available in them — but agents need an Inbox seat before a canvas app renders for them, which is free in a dev workspace and not obvious.

Surface and Placement

API Behaviour Traps

Events

Permissions

Verified live against a development workspace (2026-08-20/21) with reduced-grant probe apps, not read off a permissions page. The connector's standing minimum: Read tickets, Write tickets, Read and write users (contact search), Write users and companies (despite the name, the scope that creates and updates contacts — the synthetic partner contact needs it once), and Write conversations. Intercom additionally grants four greyed, un-uncheckable baseline scopes to every app, so reads of segments, companies, tags and admins succeed regardless and are not a misconfiguration.

Object Model and Tenancy

Vendor naming. The company renamed itself Fin in May 2026; the helpdesk product is still called Intercom, and the developer platform is documented as the Intercom and Fin Developer Platform. Expect all three names in the official documentation.

Environments

Hosts, Events, and Versions

Connect API Environments

Credentials arrive through TSANet staff until a member is on the self-service integration portal, which was announced for the September 2026 release; that has not been re-verified for this page.

EnvHost
Betaconnect2.tsanet.net
Prodconnect2.tsanet.org

testSubmission stays a per-deployment config flag, default true: creating real partner cases is a deliberate opt-in at pilot time. Note the flag asymmetry — you write testSubmission and read testCase.

Webhook Event Types (v2)

  • collaboration-request.created → inbound ticket create (claimed)
  • collaboration-request.closed → mirror partner close
  • collaboration-request.response.created → partner verdict
  • collaboration-request.response.updated → partner verdict
  • collaboration-request.note.created → notes slice

All prefixed org.tsanet.connect.. Build against v2 only: the v1 subscription and list paths sunset on 2027-01-01 — inside this connector's first year — while the v1 case paths stay with no clock, and v1 carries no event-level idempotency key at all.

Since the last verification

Recent Changes, and What Is Tracked Next

Landed (August–October 2026)

  • First full design-level security review against the connector security taxonomy (supply chain, credential lifecycle, authentication boundaries, data flow and retention, audit, untrusted-content rendering), with the authentication boundary live-probed. Findings are filed as security-labelled issues in the repository; several of the items below are its outcomes.
  • Hardened deployment profile: no webhook topics (verified functionally free), tickets-only mode, verified-minimum grants with a documented provision-then-revoke step — all in the deployment skill's intercom-setup.md.
  • SDK-cache retention sweep: age-based eviction of terminal-case content on a per-deployment window, default off (see the persistence table). The eviction itself now lives in connect-library (since 0.2.0); the connector keeps only the schedule and the window, and its interim hand-rolled sweep is retired.
  • Backups gained history: hourly dated snapshots on a 48-hour window beside the 5-minute latest copy, with a delete-first restore procedure that survives a snapshot lacking a WAL file.
  • Engine and library at 3.0.0 (2026-10-20): the connector moved to gateway-engine 3.0.0 and connect-library 3.0.0. On first start the library adds four columns to the SDK cache's note table automatically; that migration was probed against an existing 2.0.0 cache. The connector's first release to carry them is v0.2.0 (2026-10-21), also its first attested release; its notes include an "Upgrading a deployment" section.
  • Build provenance: the deployable image is built in CI from a repository commit and pushed with a secretless, registry-scoped identity (no admin credential); images are addressed by digest. Dependency updates are automated; workflow actions are SHA-pinned.
  • Registry pull by managed identity (2026-10-21): the reference deployment pulls its image with a user-assigned identity that can only pull, the registry's admin user is disabled, and deploys take only attested release images. Checked against the live registry and app on 2026-10-24.
  • A self-hosting guide (2026-10-21): installing, upgrading, rolling back and migrating a deployment in a member's own Azure or AWS account, written for an engineer with no prior knowledge of the code. It is what the hosting options section summarizes, and it is provided at onboarding.

Tracked Next (Not Promises)

  • Notes in both directions and status sync still wait on estate-wide decisions shared with the Zendesk connector: a note summary convention, severity mapping, and a platform direction field on notes. That last one now exists on Beta, where notes carry a direction, the author's company and a note type. Production's specification has carried the same fields since 2026-10-26 (spec-read 2026-10-28); Production has not been probed.
  • Outbound closure policy — see Known gaps.

Evidence standard: everything under "Landed" is merged and was probed; everything under "Tracked next" has an open issue and no merged code.

Honesty section

Deliberately Not Built, and Known Gaps

  • Outbound closure: an agent resolving the ticket does not close the partner's case. This needs a policy decision on which Intercom state transitions propagate — /closure documents only 200/400, so a wrong guess is indistinguishable from any bad request.
  • Status sync: the poller recovers missing tickets, not missing state.
  • Notes in both directions, and the information-response reply leg, land with the notes slice; note.created drains unclaimed today.
  • No ready-to-pull image for self-hosting: a self-hosting member builds the image from the release tag, and the build needs a token for TSANet's private package registry. A published image would make install and upgrade a pull, with nothing else in the procedure changing. AWS and other non-Azure platforms are also unexercised; see hosting options.
  • End-to-end encryption is not implemented: neither the engine nor this adapter encrypts, signs or verifies protected fields yet; creates carry the encryption fields empty. See the note under hosting options.
  • Multi-case tickets render no panel: byTicketId correctly refuses to pick between several cases, but the agent currently sees nothing at all. A "this ticket has N collaboration cases" panel is the follow-up.
  • Inbound ambiguity is transient: an ambiguous response action tells the acting agent, but the recorded state has moved, so a later render shows a cleanly answered case. Outbound has UNRESOLVED for this; inbound has no equivalent yet.
  • Dedupe strategy beyond the CloudEvent id is a later decision; the reconciliation poller remains the source of truth in both directions.
  • PENDINGACTION is thinly documented in both contracts; what this adapter should do with it is an open question for TSANet.
  • Attachments are not covered on this page. This adapter does not implement the contract's forwardAttachments or AttachmentDeliveryHandler: no code in the repository references either (checked at 417af2f); see moving files between members.
  • Orphaned claims hold the poller cursor loudly and indefinitely until a human releases them. Alert on consecutive held cycles, not on consecutive failures.

Build with an assistant

The Deployment Skill, and How to Load It

The connector repository — private during early access, with access granted at onboarding — ships an agent skill — skills/fin-intercom-deploy — that carries this page's operational knowledge in a form an agentic coding assistant can apply while it works on your deployment: the invariants that produce specific defects when violated (one live adapter per member, single replica, a link store that survives restarts), the Intercom workspace setup — the private app's canvas and webhook URLs, the webhook topic list (empty today, and why), the verified-minimum permissions and the hardened profile, the Back-office ticket type with its three exactly-named attributes, the per-workspace ticket-state ids — the TSANet-side provisioning, webhook subscription and go-live flag, and the environment table with a verification checklist — CONNECTOR_SDK_CACHE_RETENTION and CONNECTOR_TICKETS_ONLY, both documented on this page, were added to that table on 2026-09-14 after this page found them missing. It is plain markdown: a SKILL.md that routes, plus three reference files it points into. Every skill TSANet ships, and how to load one into any assistant, is collected on Agentic usage with TSANet Connect.

Loading It

Claude Code — copy the whole folder into your deployment project's skills directory; the references must ride along with the SKILL.md that cites them:

git clone --depth 1 https://github.com/tsanetgit/Fin-Intercom_App.git
cp -R Fin-Intercom_App/skills/fin-intercom-deploy  your-project/.claude/skills/

Other assistants — attach SKILL.md and the references/ files as project instructions; nothing in it is Claude-specific. The skill lives in the connector repository and evolves with it, so refresh your copy when you update the connector.

Typical First Steps

Boundaries worth keeping. Member credentials, Intercom client secrets and access tokens never go into an assistant session — the skill is written against placeholders and never needs a real value. And it is distilled guidance, not the source of truth: it names the repository's own files as the authority (the configuration table defers to application.yml, the reasoning to docs/decisions.md), and where they seem to disagree, the repository is newer.