Skip to content

Intercom Sensor

What it ingests

Companies, contacts, and conversations from Intercom REST API 2.13:

Intercom objectOntology entitySignal
CompanyOrganizationcompany
ContactPerson (identity)contact
ConversationConversationconversation

A company is the HubSpot company analog; a contact is identity — they resolve to Person via name and email, the same contract as a HubSpot contact. A conversation is the support thread (the Slack / Outlook analog), not an Issue: the point of ingesting Intercom is what was actually said. Each object kind has its own signal, so you can disable a noisy stream from the source's bindings without disabling the source.

This is Intercom alone. It is not a mode of HubSpot and is not bundled with other sensors in this batch.

Read-only and poll-based against https://api.intercom.io (or https://api.eu.intercom.io / https://api.au.intercom.io when region is eu or au). Intercom webhooks exist but are configured in the Developer Hub, so they cannot be provisioned at install time.

Credential

References an intercom_access_token credential by id. The secret is a private-app access token from Developer Hub → Authentication. Intercom authenticates with Authorization: Bearer <token>. There is no site URL beside the token — EU vs US vs AU is the sensor region field.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"intercom_access_token","name":"acme-intercom","secret":"<access-token>"}'

OAuth access tokens from a public marketplace app are a different product and are out of scope in v1.

Config fields

KeyRequiredNotes
enabled_objectsnoComma-separated object kinds. Blank polls every supported kind. Unknown names are rejected at configure time.
regionnous (default), eu, or au. Selects api.intercom.io vs api.eu.intercom.io vs api.au.intercom.io.
app_idnoWorkspace app id from your Intercom URL. Used only to build links back to each conversation. Leave blank to omit links.

Supported object names: companies, contacts, conversations.

There is no default_entity_type. This source emits multiple primary types, so a single override would silently mistype the rest.

Provider-side setup

  1. In Intercom, go to Settings → Integrations → Developer Hub (or Developer Hub from the avatar menu).
  2. Create an internal app, or open an existing one.
  3. Under Authentication, copy the Access token. Grant read access to conversations, contacts, and companies.
  4. Copy the token. Store only the token value, not Bearer ….
  5. If the workspace lives in the EU or AU region (app.eu.intercom.com / app.au.intercom.com), set region to eu or au.

Wire-up

POST /api/sensors

json
{
  "sensor_type": "intercom",
  "name": "Acme Intercom",
  "config": {
    "enabled_objects": "companies, contacts, conversations",
    "region": "us"
  },
  "credentials": { "access_token": "<intercom_access_token credential id>" }
}

Verify

Run Test connection. It authenticates the token against the first enabled object's list/search endpoint, so a 401/403 fails at save/test rather than on the first poll.

Once polling, each object kind's count appears in the per-poll summary log (companies_collected, contacts_collected, conversations_collected). A kind stuck at zero while the others move is the signal to check that kind's REST permission.

How incremental sync works

  • Contacts and conversations filter on updated_at (unix seconds), via POST /…/search. The per-object watermark follows that same clock. A test pins the updated_at query field.
  • Each conversation is then retrieved (GET /conversations/{id}?display_as=plaintext) so conversation_parts land in sensor_events.content. Search results do not include parts; storing only the first-message snippet would violate the ingestion-fidelity contract.
  • Companies are re-listed (POST /companies/list) and client-filtered on updated_at. Intercom has no company search-by-updated_at.
  • Each poll re-reads a two-minute overlap. List clocks can lag the write, so a strict resume would step permanently past a record that was briefly behind.
  • A page-budget hit persists a continuation cursor (starting_after) so a burst drains across ticks instead of skipping the middle.
  • Deletes are invisible. A record vanishing from results is not treated as a deletion. A conversation that 404s on retrieve (deleted between search and GET) is skipped.
  • Rate limits are shared. Intercom's published REST budget for private apps is 10,000 requests per minute per app, shared with every other integration in the workspace. The source paces itself under that floor and backs off for the cycle on a 429 rather than retrying in-loop.

Not included

  • Tickets, articles, and admins as first-class streams. Conversations already carry the support thread; tickets are a separate Intercom product. Admin identity is inferred from conversation parts.
  • OAuth / public marketplace apps. Only needed for multi-customer distribution.
  • Webhooks. Intercom supports them, but subscriptions are created in the Developer Hub, never via this sensor's install path. Polling is the supported path.