Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kick-sdk-java

An unofficial Java SDK for the Kick public API.

Covers OAuth 2.1, every documented REST resource, and webhooks — signature verification, typed event payloads and dispatch.

Full documentation →getting started · authentication · resources · webhooks · errors · recipes

  • Java 21, java.net.http.HttpClient, Jackson. No other runtime dependencies.
  • Async only: every call returns a CompletableFuture.
  • Framework-agnostic webhooks: the webhook package depends on nothing else in the SDK, so it drops into Spring, Javalin, Helidon or a bare HttpServer.

Install

<dependency>
  <groupId>io.github.andradenathan</groupId>
  <artifactId>kick-sdk-java</artifactId>
  <version>0.1.0</version>
</dependency>

Quick start

var kick = KickClient.withAppCredentials("client-id", "client-secret");

kick.categories().list(CategoryQuery.named("slots").withLimit(25))
        .thenAccept(page -> page.items().forEach(category -> System.out.println(category.name())))
        .join();

The client is immutable and thread safe. Build one and share it: it owns a single HttpClient and a token provider that renews tokens for every resource at once.

Authentication

App access token (client credentials)

For public data, when no user is involved. Tokens are fetched on the first call and renewed automatically before they expire.

var kick = KickClient.builder()
        .credentials("client-id", "client-secret")
        .build();

User access token (authorization code + PKCE)

Send the user to Kick, then exchange the code you get back. Keep state and the PKCE verifier for the callback.

var oauth = OAuthClient.builder("client-id").clientSecret("client-secret").build();

var request = AuthorizationRequest.of("https://myapp.example/callback",
        Scope.USER_READ, Scope.CHAT_WRITE, Scope.EVENTS_SUBSCRIBE);

session.put("state", request.state());
session.put("verifier", request.pkce().codeVerifier());
redirect(oauth.authorizeUrl(request));

On the callback, verify state yourself, then:

OAuthTokens tokens = oauth.exchangeCode(code, "https://myapp.example/callback", verifier).join();

var kick = KickClient.builder()
        .credentials("client-id", "client-secret")
        .userTokens(tokens)
        .onTokensRenewed(renewed -> tokenRepository.save(userId, renewed))
        .build();

Kick rotates the refresh token on every renewal, so the previous one stops working. Persist each new pair through onTokensRenewed, or a restart loses the ability to renew.

If the redirect URI host is literally 127.0.0.1, call .withLoopbackWorkaround(true) on the authorization request — Kick's frontend rewrites the first occurrence of that host, and the SDK inserts the decoy parameter that works around it.

Supplying tokens yourself

KickClient.builder().tokenProvider(() -> myVault.currentKickToken()).build();

Resources

Accessor Endpoints
kick.categories() list (v2, cursor-paginated), search and get (v1, deprecated)
kick.users() get by id, me(), introspect()
kick.channels() by user id, by slug, me(), update livestream metadata
kick.channelRewards() list, create, update, delete, redemptions, accept, reject
kick.chat() send, delete message
kick.moderation() ban, timeout, unban
kick.livestreams() list (v2, cursor-paginated), for user, stats, listV1 (deprecated)
kick.kicks() leaderboard
kick.ads() start break, status, enroll
kick.publicKey() get, build a verifier from the live key
kick.events() list, subscribe, unsubscribe webhook subscriptions
kick.drops() claims, update claims

Where Kick publishes both a v1 and a v2 of an endpoint, the unsuffixed method is v2; the v1 method stays available and is marked @Deprecated, matching how Kick documents it.

Pagination

Cursor-paginated endpoints return Page<T>:

String cursor = null;
do {
    Page<Livestream> page = kick.livestreams()
            .list(LivestreamQuery.inCategory(15).withCursor(cursor))
            .join();
    page.items().forEach(this::index);
    cursor = page.nextCursor().orElse(null);
} while (cursor != null);

Drops returns its cursor in a different place than the v2 endpoints; both are normalised into the same Page.

Webhooks

Three pieces, usable separately: a verifier, a parser and a dispatcher.

var dispatcher = WebhookDispatcher.withKickPublicKey()
        .on(ChatMessageSent.class, event ->
                log.info("{}: {}", event.sender().username(), event.content()))
        .on(ChannelFollowed.class, event -> welcome(event.follower()))
        .on(ModerationBanned.class, event -> {
            if (event.metadata().isPermanent()) {
                audit(event.bannedUser());
            }
        });

Then, in whatever serves your webhook endpoint:

// Spring MVC
@PostMapping("/webhooks/kick")
ResponseEntity<Void> receive(@RequestBody byte[] body, HttpServletRequest request) {
    dispatcher.handle(WebhookHeaders.from(request::getHeader), body);
    return ResponseEntity.ok().build();
}

Pass the raw body. Kick signs <message-id>.<timestamp>.<body>, so re-serialising a parsed payload changes whitespace and key order and the signature stops matching. The API enforces this: WebhookParser only accepts a VerifiedWebhook, which only WebhookVerifier can produce.

KickEvent is a sealed interface, so a switch over it is exhaustive:

String describe(KickEvent event) {
    return switch (event) {
        case ChatMessageSent e -> e.sender().username() + " said " + e.content();
        case ChannelFollowed e -> e.follower().username() + " followed";
        case KicksGifted e -> e.gift().amount() + " kicks from " + e.sender().username();
        case UnknownEvent e -> "unmodelled event " + e.eventName();
        default -> event.type().toString();
    };
}

UnknownEvent exists so a new Kick event type cannot break a deployed application.

The verifier uses the public key embedded in the SDK. To use the live one instead:

var verifier = kick.publicKey().verifier().join();
var dispatcher = new WebhookDispatcher(verifier, new WebhookParser());

Kick unsubscribes an app whose endpoint keeps failing for over a day, so reconcile subscriptions on startup rather than assuming they persist:

kick.events().subscribe(broadcasterUserId,
        KickEventType.CHAT_MESSAGE_SENT,
        KickEventType.CHANNEL_FOLLOWED)
    .thenAccept(results -> results.stream()
            .filter(result -> !result.succeeded())
            .forEach(result -> log.warn("{} failed: {}", result.name(), result.error())));

Subscribing reports one result per event, so a request can partly succeed.

Errors

Failures arrive as a failed future carrying an unchecked exception:

KickException
 ├── KickApiException          HTTP status, Kick's message, the raw body
 │    ├── KickAuthException       401 / 403
 │    ├── KickNotFoundException   404
 │    └── KickRateLimitException  429, exposes retryAfter()
 ├── KickTransportException    connection failure, timeout, unparseable body
 └── KickWebhookException      bad signature, missing header, malformed payload

GETs are retried on 429 and 5xx with exponential backoff and jitter — three attempts by default, honouring Retry-After when Kick sends it. Writes are never retried, so a chat message cannot be posted twice. Configure with .retryPolicy(...), disable with RetryPolicy.none().

Status

Unofficial and not affiliated with Kick. Built against the API as documented in August 2026.

About

Unofficial kick.com public API SDK in Java.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages