Intercom widgets
Show an Intercom contact with its open conversation and ticket counts, using a typed, read-only plugin on your own infrastructure.
IntercomPlugin implements the same IntegrationPlugin contract as StripePlugin and
ZendeskPlugin. It adds a contact card to a record's standard or custom overview: name,
email, role, location, sign-up and last-seen dates, the number of open conversations and
the number of open tickets, with a link to the contact's Intercom inbox. Requests travel
from the browser to your connector and then directly to Intercom. Provider credentials,
IDs and raw responses are not sent to Aviato's control plane.
These APIs require the TypeScript SDK 0.6.0, the NestJS adapter 0.4.0, and the Go and Laravel SDKs 0.7.0, together with connector 0.4.0 or later. This site tracks source code, not registry publication. Upgrade the customer connector before updating SDKs or relying on the SDK declaration: the widget manifest carries the Intercom region, and connectors without Intercom support do not show the widget (unknown providers are hidden). Installing an SDK does not install or upgrade the connector.
Configure a read-only access token
In Intercom's Developer Hub, create an app for your workspace and grant it only:
- Read and list users and companies, required for the default
externalIdbinding, which searches contacts. Read one user and one company is enough only for thecontactIdbinding, which fetches a single contact. - Read conversations, required for the open conversation count.
- Read tickets, optional. Without it the open tickets count shows "—".
Do not grant any write permission. Copy the app's access token and store it in
INTERCOM_WIDGET_TOKEN in the customer connector's process environment, using your
deployment's secret manager. The SDK receives only the variable's name. Restart the
connector when its environment changes. The integration does not perform an OAuth install
or refresh flow.
Intercom offers no API to verify a token's permissions. Unlike the Zendesk widget, the
connector cannot confirm that the token is read-only: readOnly: true is required in
configuration, but the read-only guarantee rests on the app setup above. The connector
only ever sends the requests listed under Requests per view.
Set region to the data region of your Intercom workspace: us, eu or au. It selects
a fixed Intercom API host (api.intercom.io, api.eu.intercom.io or api.au.intercom.io).
URLs and custom hosts are rejected, and the browser cannot change the region. The connector
refuses a token whose workspace reports a different known region; if Intercom reports a
region the connector does not recognize, it trusts the configured one.
Bind a record to a contact
A widget is bound to one column of the record, in one of two ways:
| Option | Column holds | Matched against |
|---|---|---|
externalId | Your own user ID | The contact's Intercom external_id |
contactId | The Intercom contact ID | The contact's Intercom ID |
Set exactly one. Using externalId with the primary key ({ field: "id" }) works when
you pass that ID to Intercom as the user's external_id. If more than one contact has the
same external_id, the widget shows an unavailable state instead of choosing one.
Who can see the widget depends on the bound column; see Visibility.
To bind through a related record (for example an order's customer), add
relation: "customer". The relation must be a belongsTo relation declared or introspected
by the connector, with one hop at most. An optional title overrides the widget heading.
TypeScript
After mounting your application plugin:
import { IntercomPlugin } from "@getaviato/sdk";
plugin.use(new IntercomPlugin({
region: "eu",
credentialEnv: "INTERCOM_WIDGET_TOKEN",
readOnly: true,
}).contact({
collection: "customers",
name: "intercom-contact",
externalId: { field: "id" },
}));Use contactId: { field: "intercom_contact_id" } instead of externalId to bind the
Intercom contact ID.
NestJS
Register the integration as a singleton feature provider. Configure one root with
AviatoModule.forRoot / forRootAsync, and bootstrap with rawBody: true as described
in the NestJS guide.
import { AviatoModule, IntercomPlugin } from "@getaviato/nestjs";
const contactFeature = AviatoModule.forFeature({
providers: [{
provide: IntercomPlugin,
useFactory: () => new IntercomPlugin({
region: "eu",
credentialEnv: "INTERCOM_WIDGET_TOKEN",
readOnly: true,
}).contact({
collection: "customers", name: "intercom-contact",
externalId: { field: "id" },
}),
}],
plugins: [IntercomPlugin],
});Import contactFeature into a Nest module. The access token itself belongs on the
customer connector.
Go
With aviato "github.com/getaviato/aviato-go" imported and your plugin configured:
intercom, err := aviato.NewIntercomPlugin(aviato.IntercomPluginOptions{
Region: "eu", CredentialEnv: "INTERCOM_WIDGET_TOKEN", ReadOnly: true,
})
if err != nil {
return err
}
if _, err := intercom.Contact(aviato.IntercomContactWidget{
Collection: "customers", Name: "intercom-contact",
ExternalID: aviato.RecordBinding{Field: "id"},
}); err != nil {
return err
}
if err := plugin.Use(intercom); err != nil {
return err
}Set ContactID instead of ExternalID to bind the Intercom contact ID. Contact returns
an error unless exactly one of the two is set.
Laravel
Register in your service provider's boot method:
use Aviato\Laravel\Facades\Aviato;
use Aviato\Laravel\Integrations\IntercomPlugin;
use Aviato\Laravel\Integrations\RecordBinding;
Aviato::use((new IntercomPlugin(
region: 'eu',
credentialEnv: 'INTERCOM_WIDGET_TOKEN',
readOnly: true,
))->contact(
collection: 'customers',
name: 'intercom-contact',
externalId: new RecordBinding(field: 'id'),
));Pass contactId: instead of externalId: to bind the Intercom contact ID. Keep the token
in the connector environment even when your application runs in Laravel.
Data and permissions
The card shows the contact's name, email, role, location, sign-up date, last-seen date, open conversations and open tickets. A null or empty binding shows an unlinked state. The browser never provides an Intercom ID directly. The inbox link is built from the region and the identifiers returned by the connector, never from a URL supplied by Intercom.
Aviato checks local record and binding-column access before making provider requests. Related bindings also require access to the source foreign-key columns, target record and target binding column.
Visibility. With the default externalId: { field: "id" } binding, anyone who can read
the record sees the contact card, because the primary key cannot be restricted. If you need
tighter control, bind to a dedicated column, for example intercom_contact_id or a column
holding the external ID, and restrict that column with roles.
Conversations and ticket contents, notes, tags, attributes and companies are not included. There are no reply, close, assign or tag actions.
Requests per view
Each view makes these requests to Intercom, using API version 2.16. The first two run together; the two counts run together once the contact is known:
GET /me, to confirm the token and read the workspace.GET /contacts/{id}for acontactIdbinding, orPOST /contacts/searchonexternal_idfor anexternalIdbinding. The search matches users only (roleuser), not leads.POST /conversations/search, counting conversations withopen = true.POST /tickets/search, counting tickets withopen = true.
The POST requests are searches and change nothing. Counts use Intercom's open flag.
Snoozed conversations and tickets are expected to count as open; this is checked against a
real workspace before release. If a count request fails, that cell shows "—" and the rest
of the card still renders. If /me or the contact lookup fails, the widget is unavailable.
A contact that was merged into another contact or deleted shows the empty state. If the workspace's app ID is unexpected, the card still renders without the "Open in Intercom" link. Intercom's search can take a short while to index newly created contacts, so a brand-new user may briefly show the empty state.
Limits and troubleshooting
All requests share a ten-second deadline. Each response is limited to one MiB, redirects
are refused, and nothing is retried. Widget responses use Cache-Control: no-store.
For an unavailable widget, check the region, the connector environment, the app's permissions, the record binding, and Intercom rate limits. A "—" in one count usually means the matching permission (Read conversations or Read tickets) is missing. No raw provider error is exposed. The normal audit trail records the local record read and its outcome, without provider IDs, credentials or response bodies.