This is a Zendesk client implementation written in Java using AsyncHttpClient and Jackson.
Start by creating a Zendesk instance
Zendesk zd = new Zendesk.Builder("https://{{your domain}}.zendesk.com")
.setUsername("...")
.setToken("...") // or .setPassword("...")
.build();
If you are behind a proxy, or want to otherwise control the lifecycle of the AsyncHttpClient instance
you should pass that through to the builder too. If you don't pass an AsyncHttpClient instance to the builder
it will create its own which will be closed by the Zendesk.close() method.
Where methods return paged data sets, an Iterable is returned that will lazy-fetch one page at a time until
all records have been fetched, so e.g.
for (Ticket ticket: zd.getTickets()) {
...
}
will iterate through all tickets. Most likely you will want to implement your own cut-off process to stop iterating when you have got enough data.
downloadAttachment(attachmentId, destination) refreshes the attachment metadata and writes
its content to a java.nio.file.Path. The overload taking an Attachment also refreshes metadata
by id rather than trusting its cached content URL. Both methods create parent directories and
replace an existing file only after receiving a successful HTTP status.
Path destination = Paths.get("downloads", "attachment.bin");
zendesk.downloadAttachment(attachmentId, destination);Downloads reuse the client's HTTP transport and authentication. Content requests do not inherit
other API headers configured through addHeader, such as Accept: application/json or Content-Type;
those headers still apply to the metadata request. Zendesk credentials are sent only
when the initial content URL has the same scheme, host and effective port as the Zendesk endpoint.
AsyncHttpClient removes authorization when a redirect crosses that origin boundary. External CDN
URLs are requested without Zendesk credentials. An injected client's redirect configuration is
respected. Responses are buffered by AsyncHttpClient before being copied; this is not a constant-memory
streaming API. Interrupted downloads cancel their request and retain the thread's interrupted flag.
The default transport disables method-agnostic connection replay so that imports, creates, uploads
and updates are not silently repeated after an ambiguous transport failure. Instead, synchronous
job reads, attachment metadata/downloads and individual pages of lazy iterables use at most three
attempts for transient connection failures, timeouts and HTTP 500/502/503/504 (1 and 2 seconds of
backoff). HTTP 429 honors Retry-After in seconds or HTTP-date form, up to a 60-second wait; longer
or negative values propagate the error without an early retry. An absent or invalid header retains
the existing 60-second fallback. Authentication, parsing and permanent failures are not retried.
Backoff waits on the synchronous caller, never a Netty event-loop thread. OAuth token minting keeps
its own provider lifecycle and is not retried by this policy. Async job methods do not add these
read retries. If an injected HTTP client has nonzero maxRequestRetry, its policy is preserved and
no additional read retry layer is added; callers must then ensure their transport replay policy is
appropriate for writes. No changes are made to setRetry, which remains a no-op.
The Zendesk API supports idempotency keys
to safely retry operations without creating duplicate resources. This client supports idempotent
ticket creation via createTicketIdempotent and createTicketIdempotentAsync.
Either method may throw a ZendeskResponseIdempotencyConflictException if the same idempotency key
is used in two requests with non-identical payloads.
The following example illustrates a usage pattern for publishing updates to a Zendesk ticket that tracks some application specific issue. It ensures that only one ticket is created per issue, even if multiple updates are published concurrently for the same issue, or if the update is retried due to a transient failure after the ticket has already been created.
class FooIssueService {
private final Zendesk zendesk;
private final Logger logger = LoggerFactory.getLogger(FooIssueService.class);
// Simple use case: the ticket payload depends only on the issue itself
public void postIssueUpdateSimple(FooIssue issue, String update) {
IdempotentResult<Ticket> result = zendesk.createTicketIdempotent(
toTicketSimple(issue),
toIdempotencyKey(issue));
if (!result.isDuplicateRequest()) {
logger.info("Created new ticket (id = {})", result.get().getId());
}
postIssueComment(result.get().getId(), update);
}
// Advanced use case: the ticket payload depends on the update
public void postIssueUpdateAdvanced(FooIssue issue, String update) {
// Fast path pre-check, would be unsafe without idempotency b/c TOCTOU.
Optional<Ticket> optTicket = findTicket(issue);
if (optTicket.isPresent()) {
postIssueComment(optTicket.get().getId(), update);
return;
}
try {
IdempotentResult<Ticket> result = zendesk.createTicketIdempotent(
toTicketAdvanced(issue, update),
toIdempotencyKey(issue));
if (!result.isDuplicateRequest()) {
logger.info("Created new ticket (id = {})", result.get().getId());
}
} catch (ZendeskResponseIdempotencyConflictException e) {
Ticket ticket = findTicket(issue).orElseThrow(
() -> new IllegalStateException(
String.format("Ticket not found for issue %s", issue.getId()), e));
postIssueComment(ticket.getId(), update);
}
}
private static Ticket toTicketSimple(FooIssue issue) {
return toTicketAdvanced(issue, "See comments for details");
}
private static Ticket toTicketAdvanced(FooIssue issue, String update) {
Ticket ticket = new Ticket(issue.getRequesterId(), issue.getTitle(), new Comment(update));
ticket.setExternalId(toIdempotencyKey(issue));
return ticket;
}
private static String toIdempotencyKey(FooIssue issue) {
// Must map the issue 1-to-1, so that retries for the same issue use the same key.
return String.format("foo-issue-%s", issue.getId());
}
private void postIssueComment(long ticketId, String update) {
Comment comment = zendesk.createComment(ticketId, new Comment(update));
logger.info("Added comment (id = {}) to ticket (id = {})", comment.getId(), ticketId);
}
private Optional<Ticket> findTicket(FooIssue issue) {
Iterator<Ticket> ticketsIt = zendesk.getTicketsByExternalId(issue.getId()).iterator();
return ticketsIt.hasNext()
? Optional.of(ticketsIt.next())
: Optional.empty();
}
}Here is the status of the various API components:
- Tickets ✓
- Ticket Audits ✓
- Incremental Export - Partial (tickets, users, organizations only)
- Ticket Fields ✓
- Ticket Import ✓
- Ticket Metrics ✓
- Ticket Forms getTicketForm() and getTicketForms()
- Views
- Users ✓
- User Fields - Partial - List User Fields (
getUserField()) - Requests ✓
- User Identities ✓
- Groups ✓
- Group Membership ✓
- Custom Agent Roles ✓
- Organizations ✓ except for related info
- Search ✓ except for topics and sort ordering
- Tags
- Forums ✓
- Forum Subscriptions
- Categories
- Topics ✓
- Post Comments
- Content Subscriptions
- Help Center Categories ✓
- Help Center Sections ✓
- Help Center Articles ✓
- Help Center Translations - Partial (List Translations, Update Translation, Delete Translation)
- Help Center Subscriptions
- Help Center Management Permission Groups
- Help Center User Segments
- Topic Votes
- Account Settings
- Activity Stream
- Attachments ✓
- Automations ✓
- Job Statuses ✓
- Locales - Partial (List Locales)
- Macros ✓ except for restrictions
- Satisfaction Ratings ✓
- Sharing Agreements
- Suspended Tickets
- Triggers ✓
The current version of this project supports Java 11 and above. It is built on Java 11 and Java 17. The release is built using Java 11.
Latest version supporting Java 8: 0.24.3 (https://github.com/cloudbees-oss/zendesk-java-client/releases/tag/zendesk-java-client-0.24.3).
- See releases