Intercom Sensor
What it ingests
Companies, contacts, and conversations from Intercom REST API 2.13:
| Intercom object | Ontology entity | Signal |
|---|---|---|
| Company | Organization | company |
| Contact | Person (identity) | contact |
| Conversation | Conversation | conversation |
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.
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
| Key | Required | Notes |
|---|---|---|
enabled_objects | no | Comma-separated object kinds. Blank polls every supported kind. Unknown names are rejected at configure time. |
region | no | us (default), eu, or au. Selects api.intercom.io vs api.eu.intercom.io vs api.au.intercom.io. |
app_id | no | Workspace 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
- In Intercom, go to Settings → Integrations → Developer Hub (or Developer Hub from the avatar menu).
- Create an internal app, or open an existing one.
- Under Authentication, copy the Access token. Grant read access to conversations, contacts, and companies.
- Copy the token. Store only the token value, not
Bearer …. - If the workspace lives in the EU or AU region (
app.eu.intercom.com/app.au.intercom.com), setregiontoeuorau.
Wire-up
POST /api/sensors
{
"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), viaPOST /…/search. The per-object watermark follows that same clock. A test pins theupdated_atquery field. - Each conversation is then retrieved (
GET /conversations/{id}?display_as=plaintext) soconversation_partsland insensor_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 onupdated_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.