Introducing the Hookbase Java SDK: hookbase-java 1.0 on Maven Central
The official Java SDK for Hookbase is on Maven Central as app.hookbase:hookbase-java. Send outbound webhooks, manage inbound sources, verify signatures, and get lazy pagination, a sealed exception hierarchy and retries that never repeat an unsafe write.
Java Gets a First-Class Client
Hookbase already had official SDKs for Node.js, Python, Go and .NET. If you ran on the JVM, you were writing your own HTTP calls against the REST API: building auth headers, mapping error bodies, deciding which failures were safe to retry, and implementing Standard Webhooks verification by hand.
Today hookbase-java 1.0.0 is on Maven Central. It covers the same API surface as the other SDKs: outbound (applications, endpoints, event types, subscriptions, messages, portal tokens, DLQ), inbound (sources, destinations, routes, events, deliveries, transforms, filters, schemas, the provider catalog) and admin (analytics, API keys, tunnels, cron jobs).
Installation
The SDK requires Java 25 or newer. Its only dependency is Jackson (jackson-databind); HTTP goes through the JDK's own java.net.http.HttpClient.
Gradle (Kotlin DSL):
dependencies {
implementation("app.hookbase:hookbase-java:1.0.0")
}
Maven:
<dependency>
<groupId>app.hookbase</groupId>
<artifactId>hookbase-java</artifactId>
<version>1.0.0</version>
</dependency>
Client Setup
Create one client per API key and share it. The client is immutable and thread-safe.
import app.hookbase.Hookbase;
import java.time.Duration;
// Defaults: https://api.hookbase.app, 30 s timeout, 3 retries
Hookbase hookbase = new Hookbase(System.getenv("HOOKBASE_API_KEY"));
// Or configure it
Hookbase custom = Hookbase.builder()
.apiKey(System.getenv("HOOKBASE_API_KEY")) // whr_...
.timeout(Duration.ofSeconds(30))
.maxRetries(3)
.build();
Every call is blocking, which keeps the API simple and suits virtual threads: run calls on Executors.newVirtualThreadPerTaskExecutor() when you want many at once.
Sending an Outbound Webhook
The outbound model is the same as in every other SDK: an application per customer, one or more endpoints where that customer receives webhooks, event types, and subscriptions that connect them. Then you send an event and Hookbase fans it out to every subscribed endpoint.
import app.hookbase.models.*;
import app.hookbase.params.*;
import java.util.Map;
// One application per customer, keyed by your own id
Application app = hookbase.applications().getOrCreate("customer_123",
new CreateApplicationParams().name("Acme Corp"));
// The signing secret is returned only on create: hand it to your customer
Endpoint endpoint = hookbase.endpoints().create(app.id(), new CreateEndpointParams()
.url("https://acme.example.com/webhooks"));
String secret = endpoint.secret();
EventType orderCreated = hookbase.eventTypes().getOrCreate("order.created",
new CreateEventTypeParams().description("An order was placed"));
hookbase.subscriptions().create(app.id(), new CreateSubscriptionParams()
.endpointId(endpoint.id())
.eventTypeId(orderCreated.id()));
SendMessageResponse sent = hookbase.messages().send(app.id(), new SendMessageParams()
.eventType("order.created")
.payload(Map.of("orderId", "ord_123", "amount", 99.99))
.idempotencyKey("order-ord_123-created"));
System.out.println(sent.eventId() + " queued to " + sent.messagesQueued() + " endpoint(s)");
Request bodies are built with fluent params classes, and responses are immutable Java records in app.hookbase.models. Records that carry credentials, like Endpoint and SourceWithSecret, print them as <redacted> in toString(), so logging one won't leak a secret.
Every send carries an idempotency key: the one you set, else one from RequestOptions, else a UUID generated for that call. It goes in both the body and the Idempotency-Key header, and the API deduplicates on it. So when the SDK retries a send after a timeout, the event is still delivered only once.
Verifying Incoming Webhooks
On the receiving side, Webhook verifies Hookbase's Standard Webhooks signatures. It checks the webhook-id, webhook-timestamp and webhook-signature headers (names are case-insensitive), rejects timestamps more than 5 minutes from now, and accepts any of the space-separated signatures sent during a secret rotation.
import app.hookbase.errors.WebhookVerificationException;
import app.hookbase.webhooks.Webhook;
import com.fasterxml.jackson.databind.JsonNode;
Webhook webhook = new Webhook(System.getenv("HOOKBASE_WEBHOOK_SECRET")); // whsec_...
// Spring Boot
@PostMapping("/webhooks")
ResponseEntity<String> receive(@RequestBody byte[] body, @RequestHeader HttpHeaders headers) {
try {
JsonNode event = webhook.verify(body, headers.toSingleValueMap());
System.out.println("Event: " + event.path("type").asText());
return ResponseEntity.ok("OK");
} catch (WebhookVerificationException e) {
return ResponseEntity.status(401).body("Invalid signature");
}
}
Take the body as byte[], not a String or a parsed object. JSON that was parsed and re-serialized no longer matches the signature. And because Hookbase sends application/json without a charset, a servlet container decodes text bodies as ISO-8859-1, which breaks any payload with non-ASCII characters. Raw bytes avoid both problems.
You can pass a Duration to change the tolerance, or a class to bind the payload directly: webhook.verify(body, headers, OrderEvent.class). For local tests, webhook.generateTestHeaders(payload) returns the headers a real delivery would carry.
Pagination
Every paginated list has two forms. list returns one Page<T> (with data(), total(), hasMore()), and listAll returns a lazy Pager<T> that fetches the next page only as you iterate:
Page<Application> page = hookbase.applications().list(new ListApplicationsParams().limit(50));
for (OutboundMessage m : hookbase.messages().listAll(app.id(),
new ListMessagesParams().status("failed"))) {
List<MessageAttempt> attempts = hookbase.messages().listAttempts(app.id(), m.id());
System.out.println(m.id() + ": " + attempts.size() + " attempt(s)");
}
// Pager is Iterable, and also has stream()
long disabled = hookbase.endpoints().listAll(app.id()).stream()
.filter(Endpoint::isDisabled)
.count();
Iterating the same Pager twice fetches the pages twice, so keep the result if you need it again.
Errors
Every failure is an unchecked HookbaseException, and the hierarchy is sealed: one class per failure mode.
HookbaseApiException: any error response, withstatus(),code()anddetails(). Subclasses:AuthenticationException(401),ForbiddenException(403),NotFoundException(404),ConflictException(409),ValidationException(400 / 422, withfieldErrors()) andRateLimitException(429, withretryAfter())HookbaseTimeoutException: the request timed outHookbaseNetworkException: the connection could not be made, or droppedWebhookVerificationException: a signature check failed
try {
hookbase.endpoints().get(app.id(), "ep_missing");
} catch (NotFoundException e) {
System.out.println("No such endpoint");
} catch (ValidationException e) {
System.out.println("Invalid: " + e.fieldErrors());
} catch (HookbaseApiException e) {
System.out.println(e.status() + " " + e.code() + ": " + e.getMessage());
}
Retries That Don't Repeat Writes
Failed requests are retried up to maxRetries times (default 3), with rules that keep a retry from doing something twice:
- 429 is retried after the
Retry-Afterthe API sends, as long as it is 60 seconds or less. A 429 without one, such as a plan quota, is thrown immediately. - 500, 502, 503, 504, timeouts and dropped connections are retried with exponential backoff (1 s, 2 s, 4 s, up to 10 s, plus up to 1 s of jitter) for every method except POST. A POST may already have gone through, so it is retried only when sending an event, where the API deduplicates on the idempotency key, or when you set
RequestOptions.retriesyourself. - A connection that was never made (refused, DNS failure) is always retried, because the request never reached the server.
- Every other error is thrown straight away.
Any call can take a RequestOptions as its last argument to override the defaults for that request:
hookbase.messages().send(app.id(),
new SendMessageParams().eventType("order.created").payload(Map.of("orderId", "ord_123")),
RequestOptions.builder()
.timeout(Duration.ofSeconds(60))
.retries(5)
.build());
Get Started
Add the dependency, create an API key under Settings → API Keys, and send your first event. The Java SDK reference covers every resource, and Webhook Signature Verification explains the signing scheme your customers verify against. The source, plus runnable examples for sending, verifying, inbound sources and error handling, is at github.com/HookbaseApp/hookbase-java.