Skip to content

Zendesk Sensor

What it ingests

Tickets, comments, and organizations from Zendesk Support:

Zendesk objectOntology entitySignal
OrganizationOrganizationorganization
TicketIssueticket
CommentCommentcomment

A Zendesk organization is a customer company (the HubSpot company analog); a ticket is the support case. Requesters, assignees, and commenters are identity — they resolve to Person via email/name, the same contract as a HubSpot contact. Each object kind has its own signal, so you can disable a noisy stream (comments, typically) from the source's bindings without disabling the source.

Ticket entity names are the Zendesk id with a # prefix (#42), matching Jira's key-as-name so a comment's RELATED_TO resolves against the same Issue the ticket event upserted. The human subject lives on the event title, not the canonical name.

This is Zendesk alone. Intercom is a different product with a different API and is not a mode of this type.

Read-only and poll-based against https://{subdomain}.zendesk.com/api/v2. Zendesk webhooks exist but are configured in the Zendesk Admin UI, never via API, so they cannot be provisioned at install time.

Credential

References a zendesk_api_token credential by id. The secret is a Zendesk API token (Admin Center → Apps and integrations → APIs → Zendesk API). Zendesk authenticates with HTTP Basic: username {email}/token, password the token. The agent email and subdomain live on the sensor config, not in the vault.

bash
curl -sS -X POST https://your-corveil-host/api/credentials \
  -H "Authorization: Bearer <admin>" -H "Content-Type: application/json" \
  -d '{"type":"zendesk_api_token","name":"acme-zendesk","secret":"<api-token>"}'

OAuth access tokens are a different product and are out of scope in v1.

Config fields

KeyRequiredNotes
subdomainyesThe label in https://{subdomain}.zendesk.com. No scheme, path, or extra dots — Corveil interpolates it into that host (SSRF).
emailyesEmail of the Zendesk agent who created the API token. Sent as the Basic-auth username {email}/token.
enabled_objectsnoComma-separated object kinds. Blank polls every supported kind. Unknown names are rejected at configure time.

Supported object names: organizations, tickets, comments.

There is no default_entity_type. This source emits multiple primary types, so a single override would silently mistype the rest.

There is no site_url. A pasted Zendesk URL is stripped to the subdomain; anything that is not a single DNS label is rejected.

Provider-side setup

  1. In Zendesk, go to Admin Center → Apps and integrations → APIs → Zendesk API.
  2. Enable Token access if it is off. Add an API token. Copy the token once.
  3. Store only the token, not email/token:token and not a Basic … header.
  4. The email on the sensor is the agent who owns that token.

Wire-up

POST /api/sensors

json
{
  "sensor_type": "zendesk",
  "name": "Acme Zendesk",
  "config": {
    "subdomain": "acme",
    "email": "agent@acme.com",
    "enabled_objects": "organizations, tickets, comments"
  },
  "credentials": { "api_token": "<zendesk_api_token credential id>" }
}

Verify

Run Test connection. It authenticates against GET /api/v2/users/me.json, so a 401/403 fails at save/test rather than on the first poll.

Once polling, each object kind's count appears in the per-poll summary log (tickets_collected, comments_collected, organizations_collected). A kind stuck at zero while the others move is the signal to check that kind's Zendesk permission.

How incremental sync works

  • First poll seeds the watermark and emits nothing. Historical ingest is the worker's one-shot Backfill (SENSOR_BACKFILL_SINCE, default 72h), matching Gong/Zoom so connecting a busy Zendesk account does not dump the entire ticket history on the first tick.
  • Tickets and comments use cursor-based incremental exports. The first real fetch sends start_time; subsequent pages and polls send after_cursor. A test pins the start_time then cursor query parameters.
  • Organizations use the time-based incremental export. The next start_time is the response end_time. next_page (an absolute URL) is not followed — Authorization stays on this client's host.
  • Tickets sideload users and organizations so requester, assignee, and company names land on the ticket event without N+1.
  • Comments come from incremental ticket events with include=comment_events, not from per-ticket comment fetches. Ticket events do not sideload users, so comment authors are resolved with GET /api/v2/users/show_many.json (regular Support API, not the incremental-export budget). A 401/403 there aborts the cycle with the comments cursor unadvanced.
  • Deletes are skipped. A ticket with status=deleted or an organization with deleted=true is not ingested.
  • Rate limits are shared. Zendesk's incremental-export budget is 10 requests per minute per account, shared with every other integration. The source paces itself under that floor and backs off for the cycle on a 429 rather than retrying in-loop.

Not included

  • Intercom. A different product; do not configure this source against Intercom.
  • OAuth / marketplace apps. Only needed for multi-customer marketplace distribution.
  • Webhooks / Triggers. Zendesk supports them, but subscriptions are created in the Zendesk UI, never via API — so they cannot be provisioned at install time. Polling is the supported path.
  • Talk, Chat, Guide, Sunshine Conversations. Out of v1. Support tickets, comments, and organizations are the structured floor.
  • Attachments, custom fields, macros, SLA metrics. Deferred.