Zendesk
What the Zendesk connector reads, with which permissions, what it never touches, how it syncs, and the facts it produces.
Last changed Markdown versionllms.txt
Source module: app/connectors/zendesk.py. Kind: facts. Display name: "Zendesk".
Purpose
Pulls tickets, ticket audits and satisfaction ratings as facts: the evidence the zendesk_ai_automated_resolution contract checks claims against. Zendesk AI agents bill per automated resolution, but the automated-resolutions report is not exposed through the API; it is downloaded from Zendesk and uploaded as the claims of a reconciliation (POST /v1/reconciliations/{id}/claims). This connector never produces claims.
Auth and scopes
| Item | Value |
|---|---|
| Method | Preferred: an OAuth client access token sent as Authorization: Bearer (Admin Center → Apps and integrations → APIs → OAuth clients; grant read scopes only). Until 2027-04-30 also: API token with HTTP Basic auth, base64("{email}/token:{api_token}"). Both against https://{subdomain}.zendesk.com. Zendesk retires API tokens on 2027-04-30; connections on an API token should move to an OAuth token before then by re-authenticating with the new credential. |
| Credentials stored (encrypted) | access_token, or email + api_token; subdomain may be stored either in credentials or in non-secret config; optional base_url |
| Least-privilege scopes | A dedicated read-only agent whose token grants tickets:read, ticket_audits:read, satisfaction_ratings:read, users:read (used only to obtain the requester's email for hashing and the audit author's role). No write scopes. |
| Connection test | GET /api/v2/users/me.json (name and role recorded in health) |
| Non-secret config | subdomain |
Fields pulled and why
| Field | Used for |
|---|---|
ticket.id, created_at, updated_at, status |
Subject id, opening message timestamp, watermark |
ticket.via.channel |
Channel for exclusions |
ticket.requester_id → users[].email |
HMAC-hashed into customer_hid |
ticket.satisfaction_rating.score |
CSAT fact (good/bad) |
audit.id, created_at, author_id → users[].role |
Fact timestamps and actor role (end-user, agent, admin, system) |
audit.events[] of type Comment (public flag) |
Customer message vs human reply |
audit.events[] of type Change on status (previous_value, value) |
Status changes and reopens |
audit.events[] of type Change on assignee_id |
Escalation |
Comment bodies are not stored.
What is hashed / never stored
- Requester email →
customer_hid(HMAC-SHA256, per-tenant key); the email is discarded. - Comment bodies, requester names, phone numbers, custom fields, tags and internal notes are not persisted (
public = falsecomments are skipped entirely). - The API token is stored only as ciphertext.
Incremental sync and watermark
- Tickets: cursor-based incremental export
GET /api/v2/incremental/tickets/cursor.json?start_time=<watermark epoch>&include=users, followingafter_urluntilend_of_stream. - For each ticket the connector fetches
GET /api/v2/tickets/{id}/audits.json(one request per ticket). - Watermark = maximum ticket
updated_at. Upserts are idempotent; re-syncing a ticket refreshes its facts.
Rate-limit handling
Shared HttpClient: honors Retry-After on 429, exponential backoff (min(60, 2^attempt) s) on 429/5xx, 6 attempts, immediate reauth failure on 401/403. Zendesk's incremental export is itself rate-limited to a small number of requests per minute; long backfills take proportionally long.
Fact types produced
Subject kind: ticket. Every fact carries customer_hid and channel.
fact_type |
When | Extra attributes | raw_ref |
|---|---|---|---|
customer_message |
Ticket creation (part: "description"); public Comment event whose author role is end-user |
— | zendesk:ticket:{id}:created, zendesk:audit:{id}:{audit_id}:{i} |
human_reply |
Public Comment event whose author role is agent or admin |
author: "agent" |
zendesk:audit:… |
status_change |
Change event on status |
from, to (raw Zendesk statuses, e.g. open, pending, solved, closed), `actor: customer |
system |
reopen |
Status change from solved/closed to open/pending/new |
by: <role> |
zendesk:audit:…:reopen |
escalation |
Change event on assignee_id with a non-empty value |
to: "human" |
zendesk:audit:… |
csat_response |
satisfaction_rating.score is good or bad |
`score: 1 | 0, scale: 1, sentiment: positive |
Claims
None. Export the automated-resolutions report from Zendesk and upload it to the reconciliation for the period; map the ticket id column to subject_id as zendesk_ticket_{ticket_id} and the resolution timestamp to claimed_at. The mapping is remembered for the vendor.
Subject id format
zendesk_ticket_{ticket_id} for both claims and facts. Internal-agent spans that refer to Zendesk tickets should use the same format (see docs/otel-convention.md).
Known limitations
- Claims come only from the uploaded automated-resolutions report; Zendesk offers no API for it.
- The satisfaction rating carries no timestamp in the ticket payload; the connector uses the ticket's
updated_at. - One audits request per ticket (N+1); very large backfills are slow.
- Assignment to a group without an assignee is not treated as an escalation; assignment from one agent to another is.
- Comment bodies are not stored, so transcript assist cannot use Zendesk data unless a CSV with
keep_textis uploaded.