Zendesk widgets
Display Zendesk users and support tickets with typed, read-only plugins on your own infrastructure.
ZendeskPlugin implements the same IntegrationPlugin contract as StripePlugin.
Add a user profile, a user's requested tickets, or a single ticket beneath a record's
standard or custom overview. Requests travel from the browser to your agent and then
directly to Zendesk. Provider credentials, tenant configuration, IDs and raw responses
are not sent to Aviato's control plane.
These APIs require the TypeScript, Go and Laravel 0.3.0 SDKs and the 0.2.0 NestJS adapter. This site tracks source code, not registry publication. Upgrade the customer agent to a build supporting Zendesk widgets before updating SDKs: the extended widget manifest includes provider account metadata that older agents may reject. Installing an SDK does not install or upgrade the agent.
Configure a read-only OAuth token
Create a Zendesk OAuth access token belonging to an agent with access to the relevant
users and tickets. Grant users:read for profiles and tickets:read for tickets.
A global read scope also works, but resource-specific scopes are preferable.
Do not grant write, resource write scopes, or impersonate.
See Zendesk's OAuth setup.
Store the token in ZENDESK_WIDGET_TOKEN in the customer agent's process environment,
using your deployment's secret manager. The SDK receives only that variable's name.
Restart the agent when its environment changes. API tokens with email/password-style
Basic authentication are not supported.
Before each widget read, the adapter calls Zendesk's
current-token endpoint
and verifies the returned scopes. It refuses non-read scopes, missing required scopes,
and failed or malformed verification responses before requesting user or ticket data.
readOnly: true is required in configuration; the agent also checks the actual token.
Token properties and refresh tokens are never returned to the browser or persisted.
Refresh/renewal of an expired token is managed by your deployment; this integration does
not perform an OAuth login or refresh flow.
Set subdomain to the label before .zendesk.com, such as northstar. Full URLs,
custom domains, ports, paths and redirects are rejected. The tenant is fixed by SDK
configuration and cannot be supplied through the browser.
TypeScript
After mounting your application plugin:
import { ZendeskPlugin } from "@getaviato/sdk";
const zendesk = new ZendeskPlugin({
subdomain: "northstar",
credentialEnv: "ZENDESK_WIDGET_TOKEN",
readOnly: true,
});
const userId = { field: "zendesk_user_id" };
plugin.use(zendesk
.user({ collection: "customers", name: "support-user", userId })
.tickets({ collection: "customers", name: "support-tickets", userId })
.ticket({
collection: "issues", name: "support-ticket",
ticketId: { field: "zendesk_ticket_id" },
}));.user() and .tickets() require userId; .ticket() requires ticketId.
To show a customer's tickets on an order, use collection: "orders" and
userId: { field: "zendesk_user_id", relation: "customer" }. The relation must be a
belongsTo relation declared or introspected by the agent. Only one relation hop is
supported. An optional title overrides the localized widget heading.
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, ZendeskPlugin } from "@getaviato/nestjs";
const supportFeature = AviatoModule.forFeature({
providers: [{
provide: ZendeskPlugin,
useFactory: () => new ZendeskPlugin({
subdomain: "northstar",
credentialEnv: "ZENDESK_WIDGET_TOKEN",
readOnly: true,
}).tickets({
collection: "customers", name: "support-tickets",
userId: { field: "zendesk_user_id" },
}),
}],
plugins: [ZendeskPlugin],
});Import supportFeature into a Nest module. Use standard Nest provider injection for
non-secret configuration; the OAuth token itself belongs on the customer agent.
Go
With aviato "github.com/getaviato/aviato-go" imported and your plugin configured:
zendesk, err := aviato.NewZendeskPlugin(aviato.ZendeskPluginOptions{
Subdomain: "northstar", CredentialEnv: "ZENDESK_WIDGET_TOKEN", ReadOnly: true,
})
if err != nil {
return err
}
zendesk.Tickets(aviato.ZendeskUserWidget{
Collection: "customers", Name: "support-tickets",
UserID: aviato.RecordBinding{Field: "zendesk_user_id", Relation: ""},
})
if err := plugin.Use(zendesk); err != nil {
return err
}User and Tickets take ZendeskUserWidget; Ticket takes ZendeskTicketWidget
with a TicketID binding. Set Relation: "customer" to resolve a related record.
Laravel
Register in your service provider's boot method:
use Aviato\Laravel\Facades\Aviato;
use Aviato\Laravel\Integrations\RecordBinding;
use Aviato\Laravel\Integrations\ZendeskPlugin;
Aviato::use((new ZendeskPlugin(
subdomain: 'northstar',
credentialEnv: 'ZENDESK_WIDGET_TOKEN',
readOnly: true,
))->tickets(
collection: 'customers',
name: 'support-tickets',
userId: new RecordBinding(field: 'zendesk_user_id'),
));Use user(..., userId: ...) for a profile and ticket(..., ticketId: ...) for a
single ticket. new RecordBinding(field: 'zendesk_user_id', relation: 'customer')
resolves through a relation. Keep the token in the agent environment even when your
application runs in Laravel.
Data and permissions
| Widget | Binding | Display |
|---|---|---|
| User | Zendesk user ID | Name, email, ID, creation date |
| Tickets | Zendesk requester user ID | Up to 20 requested tickets, sorted by most recently updated |
| Ticket | Zendesk ticket ID | ID, subject, status, priority, update date |
IDs must be positive decimal strings or positive safe integers. Store large IDs as strings to avoid numeric precision loss. A null or empty binding shows an unlinked state. The browser never provides a Zendesk ID directly.
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. Restrict the binding column to restrict access to support data. Zendesk additionally applies the OAuth user's own permissions.
Tickets are scoped to the bound requester; assigned, followed and CC'd tickets are not included. The adapter checks each returned requester ID and direct record ID. Comments, descriptions, attachments, internal notes, custom fields, HTML and URLs are not included. There are no reply, edit, assignment or status-change actions.
Limits and troubleshooting
Each refresh verifies scopes and loads one data response, under a shared ten-second request deadline. Each response is limited to one MiB. Lists stop at 20 records and show a notice when another page exists; pagination URLs are never followed. Text fields are truncated to 2,000 characters. The existing widget renderer displays text as text, without executing markup.
For an unavailable widget, check the subdomain, agent environment, OAuth token scopes and expiry, Zendesk user permissions, record binding, and provider rate limits. No raw provider error is exposed. The adapter does not retry or refresh tokens automatically. A missing/deleted direct record produces an unavailable state; an empty ticket list produces an empty state.
Widget responses use Cache-Control: no-store and are retained by the browser only
while mounted. The normal audit trail records the local record read and its outcome,
without provider IDs, credentials, tenant configuration or response bodies.