> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twinbay.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Zendesk

> Tickets, comments, users and search, with Zendesk's named wrappers and all three of its error envelopes

A Zendesk account over the Support API a helpdesk integration actually uses: tickets through the whole status machine, public and private comments, requesters created implicitly from an email address, users and organizations, groups and the assignment rules between them, search over Zendesk's query grammar, macros, derived ticket metrics and incremental exports. Zendesk publishes no first-class Python client, so the conformance suite pins Zendesk's own **OpenAPI document** for the Support API and validates every response against it.

**Slug:** `zendesk`

## Three things a twin of this API has to get right

Every response is wrapped in a **named key** — `{"ticket": …}`, never a bare object — so a twin answering bare objects fails a caller on its first read. Every identifier is an **integer**: `35436`, not `"35436"`, and every relation is an integer foreign key. And there are **three error envelopes**, all real and all branched on separately:

| Failure                  | Status      | Envelope                                                                                 |
| ------------------------ | ----------- | ---------------------------------------------------------------------------------------- |
| Record validation        | `422`       | `{"error": "RecordInvalid", "description": …, "details": {…}}` — `error` is a **string** |
| Malformed parameter      | `400`       | `{"error": {"title": "Invalid attribute", "message": …}}` — `error` is an **object**     |
| Pagination and throttles | `400`/`429` | `{"errors": [{"code": …, "title": …}]}` — an **array**                                   |

That inconsistency is Zendesk's, and reproducing it is the point: a client written against the real API has branches for all three, and a twin that normalised them would let a caller ship code that crashes on the shape it did not see.

## Two pagination mechanisms, both live

Zendesk is mid-migration from offset to cursor pagination and callers use both, on the same endpoints. `page[size]` opts a request into cursor pagination — `meta.has_more`, `meta.after_cursor`, `links.next` — and its absence means offset pagination, with `next_page`, `previous_page` and `count`. Offset is **capped** at 100 pages and 10,000 records, which the twin enforces with Zendesk's own error rather than truncating silently, because that cap is the exact wall a caller hits in production. Every `links.next` and every `url` points at the container's own host, so a paginating client cannot walk out of the sandbox.

## What it models

| Area                     | Modelled                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The account              | An owner who is an admin, two agents in two different groups, three end users, two organizations, a default ticket form and the fields on it — including an order-number text field and a refund-reason dropdown — plus four tickets across the status range, one of them solved long enough ago that advancing the sandbox closes it                                                                                                                                                  |
| Authentication           | Both credentials Zendesk takes: an **API token over HTTP basic** as `you@example.com/token`, and an **OAuth bearer token**. The missing `/token` suffix, password basic authentication, an unknown token and a suspended identity are all refused with Zendesk's one `401` body. Scopes gate the bearer presentation only, because an API token carries its user's whole role                                                                                                          |
| Roles                    | The role gate that is the most common real Zendesk integration failure after rate limits: an end-user credential is `403` on the agent surface and still answers `users/me`, and light agents are agents carrying a `custom_role_id`, exactly as Zendesk models them                                                                                                                                                                                                                   |
| Rate limits              | Per-endpoint budgets rather than one bucket — the account's plan-based per-minute limit, `tickets-index`, `tickets-update` (including 30 per ten minutes **per identity per ticket**), `search-index`, the exports' ten a minute, and the user-update budgets — with `X-Rate-Limit`, the newer `ratelimit-*` trio, the per-endpoint `Zendesk-RateLimit-*` headers, and a `429` carrying a truthful `Retry-After`                                                                       |
| Tickets                  | Create, retrieve, update and list; the status machine with `solved` back to `open` allowed and everything out of `closed` refused; `description` set once from the first comment; implicit requester creation from an inline email address; `submitter_id` deciding who authored that comment; tags as a set with `additional_tags` and `remove_tags`; `custom_fields` and `fields` as two names for one array; and `external_id` left non-unique because Zendesk leaves it non-unique |
| Closed tickets           | Immutable, with the follow-up that is the only correct recovery: `via_followup_source_id` produces a new ticket whose id appears in the closed one's `followup_ids`                                                                                                                                                                                                                                                                                                                    |
| Comments                 | Added through a ticket update, public or private, attributable to somebody other than the caller, with the ticket's description as the first comment and cursor pagination over the rest                                                                                                                                                                                                                                                                                               |
| Users and organizations  | Creation with Zendesk's own default of end user, the duplicate-email `422` naming the field, `create_or_update` resolving `external_id` before `email` and case-insensitively, organization membership, and an organization's tickets being the ones its members requested                                                                                                                                                                                                             |
| Groups                   | Groups and memberships, and the refusal when a ticket is assigned to an agent outside its group — plus the refusals for an end user, a suspended agent and a group that does not exist                                                                                                                                                                                                                                                                                                 |
| Search                   | Zendesk's grammar over the subset the twin models, a heterogeneous `results` array with `result_type`, its own error format, its own rate limit, offset-only paging, and the thousand-result ceiling that is a `422` rather than an empty page                                                                                                                                                                                                                                         |
| Bulk jobs                | `update_many` and `create_many` answering a `queued` job that has **not** happened yet, the `queued` → `working` → `completed` lifecycle through `advance`, per-record results, partial failure against the one bad record, and the hundred-record ceiling                                                                                                                                                                                                                             |
| Macros, metrics, exports | Applying a macro, with the preview that answers what *would* change without changing it; ticket metrics derived from the comment and status history so they cannot disagree with it; satisfaction ratings only the requester may leave; and the incremental export with its `end_of_stream` and its refusal to return the most recent minute                                                                                                                                           |

Advancing the sandbox does three things, because three things in a helpdesk age: a solved ticket becomes closed once the account's close window has passed — which no API call can trigger, and which is why `advance` exists — a queued bulk job moves towards completed, and an unanswered ticket crosses its first-reply target.

## Scenarios

`busy-helpdesk`, `wismo-queue`, `closed-ticket`, `sla-breached`, `end-user-token`, `rate-limited`, `pending-bulk-job`.

## Not modelled

Help Center and Guide, Talk and Chat, Sunshine Conversations and messaging, Side Conversations, Explore and reporting, triggers and automations, SLA policy evaluation (the twin models one first-reply target so a breach is reachable, not a policy engine), the Apps framework, webhooks, custom objects, Sell, attachments and uploads, views, and the admin APIs beyond what the sandbox's own directives need.

The Support API has no dated releases, so the twin's behaviour is pinned to the OpenAPI document's version and to the date that document and the API reference were read. No live Zendesk account was available when it was built — the trial locks after fourteen days, which is a large part of why this twin exists — so its README lists every place a live account would settle something the twin had to decide, including what Zendesk does when a ticket is assigned to an agent outside its group. A route Zendesk has and the twin has not built answers a `501` carrying a value Zendesk never sends, rather than `RecordNotFound`, which would read as missing data and send a caller away.
