/api/v2, with Zendesk’s JSON shapes and error bodies. Tickets follow the real status lifecycle, with comments and internal notes, audits, custom fields, problems and incidents, followers and CCs, satisfaction ratings, and bulk jobs. It also covers users with identities, organizations with memberships, groups, search with the Zendesk query syntax, views, triggers that notify webhooks, webhooks signed the way Zendesk signs them, and OAuth with scopes. Lists page by offset and by cursor, and every answer carries rate limit headers. The tests drive it with the official node-zendesk client (6.0.1).
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000.
Without seed config, the account has the owner Admin <[email protected]>, a Support group, the system ticket fields, and the default views.
Point node-zendesk at the emulator with endpointUri:
URL mapping
Paths work with or without the
.json suffix.
Auth
- API token:
Authorization: Basic base64("{email}/token:{api_token}")with a token fromapi_tokens[], for any active user’s email. - Password:
Authorization: Basic base64("{email}:{password}"), only whenpassword_access: true. Seed passwords withowner.password,agents[].password, andend_users[].password. - OAuth:
Authorization: Bearer {access_token}.readcovers everyGET,writeevery change, and resource scopes such astickets:readorusers:writecover one resource. - A token the CLI accepts (for example
test_token_admin) sent as a bearer token acts as the account owner.
{"error":"Couldn't authenticate you"}. A role or scope that does not cover the endpoint answers 403 {"error":"Forbidden",...}. End users may read their own tickets and comments, rate their solved tickets, and read and edit themselves; agents may not manage admins; webhooks, triggers, groups, ticket fields, and OAuth clients need an admin.
X-On-Behalf-Of: {email} makes an admin act as another user when the call uses a CLI token or an OAuth token with the impersonate scope.
OAuth
GET /oauth/authorizations/new?response_type=code&client_id=&redirect_uri=&scope=&state=shows the consent screen, where you choose the user to sign in as.code_challengeandcode_challenge_methodadd PKCE, andresponse_type=tokenreturns#access_token=(implicit grant)POST /oauth/tokenswith JSON or a form:authorization_code(withclient_secret, or acode_verifier),client_credentials(a confidential client acting as its owner), andrefresh_token. Tokens do not expire unless the request setsexpires_in(300 to 172800 seconds), which also returns a refresh token. Errors follow RFC 6749 (invalid_grant,invalid_client,unsupported_grant_type,invalid_scope)GET /api/v2/oauth/tokens,GET /api/v2/oauth/tokens/current,POST /api/v2/oauth/tokens(admins; answersfull_token),GETandDELETE /api/v2/oauth/tokens/:idGET /api/v2/oauth/clients,POST /api/v2/oauth/clients,GET /api/v2/oauth/clients/:id,PUT /api/v2/oauth/clients/:id,DELETE /api/v2/oauth/clients/:id, andPUT /api/v2/oauth/clients/:id/generate_secret
Errors
The Webhooks API answers errors in its own
{"errors": [{"code", "title", "detail"}]} envelope.
Rate limits
Every answer carriesX-Rate-Limit, X-Rate-Limit-Remaining, ratelimit-limit, ratelimit-remaining, and ratelimit-reset. Past rate_limit requests a minute (700 by default; 0 turns the limit off) the answer is 429 {"error":"APIRateLimitExceeded"} with Retry-After.
Tickets
GET /tickets,GET /tickets/count,GET /tickets/show_many?ids=,GET /tickets/:idPOST /ticketsanswers 201 withticketandaudit. The first comment is required (Description: cannot be blank)PUT /tickets/:idwithcomment,status,priority,type,assignee_id,group_id,requester_idorrequester: { email, name },tags,additional_tags,remove_tags,custom_fields,due_at,problem_id,external_id,follower_ids,email_cc_ids,collaborator_ids, andsafe_updatewithupdated_stamp(409UpdateConflictwhen the ticket changed)DELETE /tickets/:idsoft deletes;GET /deleted_tickets,PUT /deleted_tickets/:id/restore, andDELETE /deleted_tickets/:id(a job) manage deleted ticketsPOST /tickets/create_many,PUT /tickets/update_many(?ids=with oneticket, or aticketsarray with ids), andDELETE /tickets/destroy_many?ids=answer a job status. Jobs run at once; the creation answer saysqueuedandGET /job_statuses/:idsayscompletedwith per item results and errors- Tags:
GET /tickets/:id/tags,POSTreplaces,PUTadds,DELETEremoves GET /tickets/:id/collaborators,/followers,/email_ccs,/incidents,GET /problemsGET /users/:id/tickets/requested,/ccd,/assigned,/followed, andGET /organizations/:id/tickets
Status lifecycle
- A new ticket with an assignee is
open. A ticket cannot go back tonew. - Solving assigns the solving agent when no one is assigned, and offers a satisfaction rating.
- A comment from the requester reopens a
pending,hold, orsolvedticket. - Solved tickets close
solved_close_after_msafter they were solved (four days by default), through the “Close ticket 4 days after status is set to solved” automation.POST /_zendesk/simulate/close-solvedwith an optional{"ticket_ids": [...]}closes them now. - Closed is terminal: updates answer 422
Status: closed prevents ticket update. Create a follow-up withvia_followup_source_id. - Solving a problem solves its incidents.
- Setting both
group_idand anassignee_idoutside that group is refused.
Custom fields
ticket_fields[] seeds custom fields, and /ticket_fields manages them. Values are checked by type (text, textarea, checkbox, date, integer, decimal, regexp, tagger, multiselect); tagger, multiselect, and checkbox values add and remove their tags.
Comments
GET /tickets/:id/comments (with include=users and sort_order=desc), GET /tickets/:id/comments/count, and PUT /tickets/:id/comments/:comment_id/make_private. Comments take body or html_body, public (false makes an internal note), and author_id, and answer body, html_body, and plain_body. End users see only public comments. GET /tickets/:id/audits and GET /tickets/:id/audits/:audit_id list the Create, Change, and Comment events of each update, with the trigger that made a change in its via.
Satisfaction ratings
The requester rates a solved or closed ticket withPOST /tickets/:id/satisfaction_rating and {"satisfaction_rating": {"score": "good"}} (or bad, with an optional comment). GET /satisfaction_ratings (score=good|bad|received) and GET /satisfaction_ratings/:id read them.
Users, organizations, and groups
- Users: list (
role,role[]), count,me, search (queryorexternal_id), autocomplete,show_many, create,create_many,create_or_update,create_or_update_many, show, update, delete (soft). A new user joins the organization whosedomain_namesmatch the email domain - Identities: list, show, create, update,
make_primary,verify, delete - Tags on users and organizations:
GET,POSTreplaces,PUTadds,DELETEremoves.GET /tagsandGET /autocomplete/tagslist tags across the account - Organizations: list, count,
show_many, search (nameorexternal_id), autocomplete, create,create_many,create_or_update,destroy_many, show, update, delete, and their users - Organization memberships: list (all, per organization, per user), create,
create_many, show, delete, andmake_default - Groups: list, count, assignable, create, show, update, delete (soft), and their users. Group memberships: list, assignable, create,
create_many, show, delete, andmake_default. Only agents and admins can be members
Pagination
Lists page by offset withpage and per_page (at most 100), answering full next_page and previous_page URLs and count. Lists that support cursors switch to cursor mode when any page[...] parameter is present: page[size] (at most 100), page[after], page[before], answering meta: { has_more, after_cursor, before_cursor } and links: { next, prev }. node-zendesk asks for page[size]=100 and follows the links.
Search
GET /search?query= answers results with result_type, facets: null, and offset pages up to 1,000 results (beyond that, 422 Invalid search: Requested response size was greater than Search Response Limits). GET /search/count counts, and GET /search/export?query=&filter[type]=ticket pages by cursor without the limit.
The query parser is hand written and never refuses a query:
- Free text and
"quoted phrases",-to negate, and*as a wildcard keyword:value,keyword<value,keyword>value,keyword<=value,keyword>=valuetype:ticket,type:user,type:organization,type:group(every type without one)- Tickets:
statusandpriority(ordered, sostatus<solvedworks),ticket_type,tags,subject,description,comment,assignee,requester,submitter,cc,follower,commenter(me,none, an id, an email, or a name),group,organization,via,external_id,satisfaction,fieldvalue, and the datescreated,updated,solved, anddue_date(2026-01-01, an ISO time, or a relative time such as4hoursor2days) - Users:
name,email,role,organization,group,tags,phone,external_id,notes,details,is_verified,is_suspended - Organizations:
name,tags,domain,external_id,details,notes. Groups:name order_by:andsort:in the query, orsort_byandsort_orderparameters
: are alternatives (status:open status:pending); everything else must hold together.
Views
GET /views, GET /views/active, GET /views/:id, POST /views (admins), PUT /views/:id, DELETE /views/:id, GET /views/:id/tickets, GET /views/:id/execute (rows and columns), GET /views/:id/count, GET /views/count_many?ids=, and POST /views/preview. Views take all and any conditions and an output with columns, sort_by, and sort_order. Five default views (“Your unsolved tickets”, “Unassigned tickets”, and others) are seeded.
Triggers
GET /triggers, GET /triggers/active, GET /triggers/:id, POST /triggers, PUT /triggers/:id, and DELETE /triggers/:id (admins). After every ticket create and update, active triggers run in order until none fires, each at most once.
- Conditions:
update_type(Create,Change),status,priority,type,group_id,assignee_id,requester_id,organization_id,current_tags,comment_is_public,comment_includes_word,subject_includes_word,role,via_id, andcustom_fields_{id}, withis,is_not,less_than,greater_than,changed,not_changed,value,value_previous,includes,not_includes,present, andnot_present - Actions:
status,priority,type,group_id,assignee_id(current_userworks),set_tags,current_tags,remove_tags,custom_fields_{id}, andnotification_webhookwith[webhook_id, body]. The body takes placeholders such as{{ticket.id}},{{ticket.title}},{{ticket.status}},{{ticket.url}},{{ticket.requester.email}}, and{{current_user.name}}, escaped for JSON when the webhook sends JSON
Webhooks
GET /webhooks(cursor pages,filter[name_contains],filter[status],sort),POST /webhooks(201),GET /webhooks/:id,PUTandPATCH /webhooks/:id(204),DELETE /webhooks/:idGET /webhooks/:id/signing_secretandPOST /webhooks/:id/signing_secret(rotate)POST /webhooks/testsends a request and answers what the endpoint said. Withoutwebhook_idit is signed with Zendesk’s static test secretdGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==GET /webhooks/:id/invocationsandGET /webhooks/:id/invocations/:invocation_id/attempts
conditional_ticket_events (trigger notifications) or to event types: zen:event-type:ticket.created, ticket.status_changed, ticket.priority_changed, ticket.comment_added, ticket.tags_changed, ticket.agent_assignment_changed, ticket.custom_field_changed, ticket.soft_deleted, ticket.csat_received, user.created, organization.created, and the other ticket, user, and organization events. Event bodies have Zendesk’s shape (type, subject: "zen:ticket:{id}", detail, event, account_id, time, zendesk_event_version: "2022-11-06").
Each request carries X-Zendesk-Webhook-Id, X-Zendesk-Webhook-Invocation-Id, X-Zendesk-Account-Id, X-Zendesk-Webhook-Signature-Timestamp, and X-Zendesk-Webhook-Signature, which is base64(HMAC-SHA256(signing_secret, timestamp + raw_body)):
Retry-After of 60 seconds or less is retried after that delay.
Seed config
Admin <[email protected]> unless owner says otherwise), a Support group, the system ticket fields, and the default views. Seeded tickets do not run triggers or send webhooks. csat: false turns satisfaction ratings off.
Inspector
GET /_inspector shows tickets (open one for its comments and JSON), users, organizations and groups, views and triggers, webhooks with their recent invocations, and API tokens and OAuth clients and tokens.
Limits
The Help Center, Chat, Talk, Sell, macros, automations other than closing solved tickets, SLAs, ticket forms, the Requests API, attachments and uploads, incremental exports, side conversations, and the email channel.Conformance
See CONFORMANCE.md for every provider.