# Zendesk widgets (https://docs.getaviato.com/integrations/zendesk)



`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 [#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](https://developer.zendesk.com/documentation/authentication/creating-and-using-oauth-tokens-with-the-api/).

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](https://developer.zendesk.com/api-reference/ticketing/oauth/oauth_tokens/#show-current-token)
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 [#typescript]

After [mounting your application plugin](/integrations/typescript):

```ts
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 [#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](/integrations/nestjs).

```ts
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 [#go]

With `aviato "github.com/getaviato/aviato-go"` imported and your plugin configured:

```go
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 [#laravel]

Register in your service provider's `boot` method:

```php
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 [#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 [#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.
