Skip to content

Latest commit

 

History

1,334 Commits

Folders and files

Repository files navigation

Zendesk Java Client

This is a Zendesk client implementation written in Java using AsyncHttpClient and Jackson.

Java CI with Maven Maven Central Coverage Lines Quality Gate Status

Using the API

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.

Attachment downloads

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.

Read retries

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.

Idempotency

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.

Usage Example

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();
    }
}

Community

Status

Here is the status of the various API components:

JDK Support

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).

History

Releases

Packages

Used by

Contributors

Languages