Aviato / docs
Plugins & integrations

Intercom widgets

Show an Intercom contact with its open conversation and ticket counts, using a typed, read-only plugin on your own infrastructure.

View Markdown

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 externalId binding, which searches contacts. Read one user and one company is enough only for the contactId binding, 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:

OptionColumn holdsMatched against
externalIdYour own user IDThe contact's Intercom external_id
contactIdThe Intercom contact IDThe 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:

  1. GET /me, to confirm the token and read the workspace.
  2. GET /contacts/{id} for a contactId binding, or POST /contacts/search on external_id for an externalId binding. The search matches users only (role user), not leads.
  3. POST /conversations/search, counting conversations with open = true.
  4. POST /tickets/search, counting tickets with open = 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.

Edit on GitHub

On this page