Skip to content
Browse the documentation
Connectors

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 = false comments 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, following after_url until end_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_text is uploaded.