Zendesk Sensor
What it ingests
Tickets, comments, and organizations from Zendesk Support:
| Zendesk object | Ontology entity | Signal |
|---|---|---|
| Organization | Organization | organization |
| Ticket | Issue | ticket |
| Comment | Comment | comment |
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.
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
| Key | Required | Notes |
|---|---|---|
subdomain | yes | The label in https://{subdomain}.zendesk.com. No scheme, path, or extra dots — Corveil interpolates it into that host (SSRF). |
email | yes | Email of the Zendesk agent who created the API token. Sent as the Basic-auth username {email}/token. |
enabled_objects | no | Comma-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
- In Zendesk, go to Admin Center → Apps and integrations → APIs → Zendesk API.
- Enable Token access if it is off. Add an API token. Copy the token once.
- Store only the token, not
email/token:tokenand not aBasic …header. - The email on the sensor is the agent who owns that token.
Wire-up
POST /api/sensors
{
"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 sendafter_cursor. A test pins thestart_timethencursorquery parameters. - Organizations use the time-based incremental export. The next
start_timeis the responseend_time.next_page(an absolute URL) is not followed — Authorization stays on this client's host. - Tickets sideload
usersandorganizationsso 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 withGET /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=deletedor an organization withdeleted=trueis 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.