The REST API Zendesk Actually Exposes
Learn the conventions of the REST API Zendesk exposes once and every endpoint gets easier: wrapped payloads, cursor pagination, sideloading, and a small set of errors.
In the REST API Zendesk wraps requests and responses
Zendesk wraps almost everything in a key named after the resource. A single ticket comes back under ticket, a list under tickets, a user under user. You send data the same way.
{
"ticket": {
"subject": "Card declined at checkout",
"comment": { "body": "It failed three times." }
}
}Forget the wrapper and you get a 400 or a 422 with a message that doesn't obviously say "you forgot the wrapper". It's the single most common first mistake, and it costs about twenty minutes.
Updates are PUT and they are partial. Send only the attributes you want to change and everything else is left alone. With one famous exception: arrays replace rather than merge, so sending tags wipes the tags that were there. Zendesk provides separate parameters for adding and removing individual tags, and you should use them.
Content type is JSON in both directions. Set the header explicitly, because a missing Content-Type: application/json produces errors that look like validation problems.
Pagination: use cursors
There are two pagination styles in the API and only one of them is a good idea now.
Offset pagination uses page and per_page. It's simple, it's what every old code sample shows, and it degrades badly past the first few pages because the server has to count its way there. Zendesk caps how deep you can go with it.
Cursor pagination is the current recommendation. You ask for a page size and follow a link:
GET /api/v2/tickets.json?page[size]=100The response carries a meta object with has_more and an after_cursor, plus a links.next URL you can request directly. Loop until has_more is false. Don't try to reconstruct the cursor yourself, and do not store it as a bookmark for tomorrow, because it describes a position in a result set, not a point in time.
For "everything since I last looked", cursors are the wrong tool entirely. That is what the incremental export endpoints are for, and they take a start time rather than a cursor.
Sideloading, the single best trick
Records reference each other by numeric ID. A ticket has a requester_id, an assignee_id, a group_id, an organization_id, and none of those come with a name attached.
Resolve each one with its own request and a page of a hundred tickets turns into several hundred calls, most of them asking about the same twenty people. Instead, ask for the related records in the same response.
GET /api/v2/tickets.json?include=users,groups,organizationsThe response now carries users, groups and organizations arrays alongside tickets. Build a map by ID in memory and look everything up locally. Which side-loads are supported varies by endpoint, so check the reference for the one you're calling rather than assuming, but where it works the saving is enormous.
This is the difference between an integration that sits comfortably inside its rate limit and one that spends its entire budget on lookups.
The errors that mean something
A small vocabulary covers nearly everything.
email/token as the username and not just the email.Retry-After, then retry with jitter.That last point deserves emphasis. Creation isn't idempotent. A timeout after the server did the work, followed by a retry, gives you two identical records seconds apart. Store an external_id from your own system so you can tell.
Bulk, jobs and asynchrony
Anything that touches many records at once returns a job rather than data. create_many, update_many, destroy_many and their equivalents on other resources all behave this way, generally capped at 100 records per request.
You get back a job ID. Poll the job statuses endpoint until it reports as completed, then read the results array. This is the step people skip. A job can finish successfully with individual records failed inside it, and nothing tells you unless you look at the results.
Don't poll in a tight loop either. A second or two between checks is fine, and the job status counts against the same rate limit as everything else.
Frequently asked questions
Which pagination should new integrations use?
Cursor. Zendesk API pagination offers offset and cursor styles, and offset stops working past ten thousand records, so anything built today should use cursors from the first line.
How do pagination and sideloading work?
Zendesk cursor pagination is the current standard and the one to build on, because offset pagination breaks past ten thousand records. Zendesk API sideloading fetches related records in the same call, and Zendesk API errors are a small, well-documented set worth handling explicitly.
Does the Zendesk REST API use cursor or offset pagination?
Both exist, but cursor pagination is the current recommendation. Request page[size] and follow links.next until meta.has_more is false.
Why does my POST return a 400 with no obvious cause?
Usually a missing wrapper. Payloads are wrapped in a key named after the resource, so a ticket body goes inside a ticket object.
What's sideloading and where does it work?
Adding ?include= so related records come back in the same response. Supported side-loads differ per endpoint, so check the reference for the one you are calling.
Are PUT updates partial or full replacements?
Partial for scalar fields, but arrays replace. Sending tags overwrites the whole set, so use the dedicated add and remove parameters instead.
How do I know a bulk job actually worked?
Poll the job statuses endpoint and read the results array. A job can report completion while individual records inside it failed.
Conventions are easy, deduplication is not
Ticket Merger merges duplicate Zendesk tickets automatically using requester, subject keywords and ticket fields within a time window, with exclusions you control.
Start free trial14-day free trial. No credit card required.