The Zendesk Ticket API

Nearly everything you build against Zendesk runs through one resource. Here's how the Zendesk ticket API behaves, including the parts that surprise people.

The Zendesk ticket API endpoints you'll actually use

Everything hangs off /api/v2/tickets. Five calls cover most integrations.

`GET /api/v2/tickets/{id}.json` returns one ticket, with its current field values and its status.
`GET /api/v2/tickets.json` lists tickets, newest last, paginated. Use cursor pagination rather than page numbers.
`POST /api/v2/tickets.json` creates one, with the payload wrapped in a ticket object.
`PUT /api/v2/tickets/{id}.json` updates one, again wrapped, and only the attributes you send are touched.
`GET /api/v2/tickets/show_many.json?ids=1,2,3` fetches a batch in a single request, which is the cheapest way to hydrate a list of IDs you already have.

Authentication is an API token paired with an agent email address, or OAuth for anything a customer installs. Use a dedicated integration user, not a real person. You'll want to exclude its updates from triggers later, and you can't do that if the account belongs to someone who also answers tickets.

Comments are not a ticket field

This is the first thing that trips people up. A ticket doesn't carry its conversation in the object you get back from GET. You get the description, which is the text of the very first comment, and nothing else.

To read the conversation you call /api/v2/tickets/{id}/comments.json, which returns comments in order with an author_id, a public flag, attachments and, when you ask for it, the HTML body as well as the plain text.

To write one you don't post to that endpoint. You update the ticket and include a comment object in the payload, with body and public: true or public: false for an internal note. That asymmetry, reading from one place and writing to another, is the single most common cause of a first integration not working.

Comments are really ticket events underneath, which is why the audits endpoint shows every field change alongside them. Need to prove who changed a priority and when? Audits is the place.

Side-loading, and why it matters

A ticket references people and things by ID. requester_id, assignee_id, group_id, organization_id. Resolve each of those with its own request and a list of a hundred tickets becomes several hundred API calls, most of them for the same twenty users.

Append ?include=users,groups,organizations and Zendesk returns those records alongside the tickets in the same response. Build a small map from the side-loaded arrays and look everything up locally. It works on list endpoints and on single fetches. That's the difference between an integration that runs comfortably inside your rate limit and one that spends its whole budget asking who user 4417 is for the ninth time.

Fields, tags and the read-modify-write problem

Custom fields

Custom fields arrive as an array of { "id": 360001234567, "value": "..." } objects, and you set them the same way. Send only the fields you want to change. Hard-coding the numeric IDs in your source is fine for a private integration and a liability for anything you ship, since the IDs differ per account. Fetch them once from the ticket fields endpoint and cache by name.

Tags replace, they do not append

Send tags on an update and you replace the entire set. Every tag a trigger, a macro or an agent added is gone, silently, and nobody notices for a week. Zendesk provides parameters for adding and removing individual tags on an update. Use those. Don't read the array, append and write it back.

Concurrent updates

If two things update the same ticket at the same moment, the later write wins and takes the whole payload with it. For anything where that matters, include the updated_stamp value you read along with a safe-update flag so Zendesk rejects the write rather than clobbering a change you never saw.

Bulk work and rate limits

For volume there are create_many and update_many, both capped at 100 tickets per request. They return a job ID rather than tickets, and you poll the job statuses endpoint until it reports completion. Read the results array when it does, because a job can finish with some records failed and Zendesk won't tell you unless you look.

Rate limits are per account, vary by plan, and are lower than people assume. A 429 comes back with a Retry-After header, and honouring it isn't optional. Exponential backoff with jitter will save you from the afternoon where your sync locks out the mobile app your agents are using.

One more limit catches everyone eventually: closed tickets are immutable, and tickets closed long enough get archived, which makes them behave oddly in list and search results. An integration that tries to tag something a customer never replied to will fail forever, and it won't say why.

Merging over the API

Merges have their own endpoint: POST /api/v2/tickets/{id}/merge, taking the IDs of the tickets you want folded in, plus optional comments for each side. It behaves exactly like the interface version. The source tickets are closed and tagged closed_by_merge, the merge is permanent, and tickets that have already reached Solved or Closed can't take part.

That permanence is the reason to be conservative with anything automated. Merging by API is easy. Deciding correctly which two tickets are the same problem is the hard half, and it isn't an API question.

FAQ

Frequently asked questions

How do I add a comment with the Zendesk ticket API?

Update the ticket and include a comment object with a body and a public flag. There's no POST endpoint for creating a comment directly.

What is side-loading?

Adding ?include=users,groups,organizations to a request so related records come back in the same response instead of needing separate calls. It cuts request counts dramatically on list endpoints.

Why did my update wipe the ticket tags?

Sending the tags array replaces the whole set. Use the dedicated add and remove tag parameters on the update instead.

Can I update a closed ticket?

No. Closed tickets are immutable, and archived ones behave differently again in list and search results. Design for both cases rather than treating every ticket as writable.

How many tickets can I create in one request?

Up to 100 with create_many, which runs asynchronously and returns a job ID. Poll job statuses and read the results array, because individual records can fail inside a job that reports as complete.

The API will create whatever you tell it to

It has no opinion on whether that ticket already exists. Ticket Merger watches the queue and merges the pairs your integration created, before two agents pick them up.

Start free trial

14-day free trial. No credit card required.