app.hookbase:hookbase-java
The Hookbase Java SDK provides a typed, thread-safe client for sending webhooks to your customers. It requires Java 25 or newer. Its only dependency is Jackson (jackson-databind); HTTP uses the JDK's java.net.http.HttpClient.
Source: github.com/HookbaseApp/hookbase-java
Installation
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>Quick Start
import app.hookbase.Hookbase;
import app.hookbase.params.SendMessageParams;
import java.util.Map;
Hookbase hookbase = new Hookbase(System.getenv("HOOKBASE_API_KEY"));
// Send a webhook
hookbase.messages().send("app_123", new SendMessageParams()
.eventType("order.created")
.payload(Map.of("orderId", "ord_123", "amount", 99.99)));Client Configuration
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 with the builder
Hookbase hookbase = Hookbase.builder()
.apiKey(System.getenv("HOOKBASE_API_KEY"))
.baseUrl("https://api.hookbase.app") // Optional; must be https (http only for localhost)
.timeout(Duration.ofSeconds(30)) // Optional request timeout
.maxRetries(3) // Optional retry count
.build();Create one client per API key and share it: the client is immutable and thread-safe. Every call is blocking, so run calls on virtual threads (Executors.newVirtualThreadPerTaskExecutor()) to make many at once.
Request bodies are built with fluent params classes. A setter you don't call is left out of the request. On update params, setting a field to null sends JSON null, which clears the field where the API allows it.
Applications
Applications represent your customers. Each customer should have one application.
Create Application
Application app = hookbase.applications().create(new CreateApplicationParams()
.name("Acme Corp")
.externalId("customer_123") // Your internal customer ID
.metadata(Map.of("plan", "enterprise")));Get or Create Application
// Returns the existing app or creates a new one
Application app = hookbase.applications().getOrCreate("customer_123",
new CreateApplicationParams().name("Acme Corp"));List Applications
// One page
Page<Application> page = hookbase.applications().list(new ListApplicationsParams().limit(50));
// Every application, fetched page by page as you iterate
for (Application a : hookbase.applications().listAll()) {
System.out.println(a.name());
}Get by UID
Application app = hookbase.applications().getByUid("customer_123");Event Types
Event types define the webhook events your customers can subscribe to.
Create Event Type
EventType type = hookbase.eventTypes().create(new CreateEventTypeParams()
.name("order.created")
.displayName("Order Created")
.category("orders")
.schema(Map.of("type", "object", "required", List.of("orderId")))
.examplePayload(Map.of("orderId", "ord_123")));List Event Types
for (EventType t : hookbase.eventTypes().listAll(new ListEventTypesParams().category("orders"))) {
System.out.println(t.name());
}Subscriptions
// Subscribe an endpoint to an event type
Subscription sub = hookbase.subscriptions().create("app_123", new CreateSubscriptionParams()
.endpointId("ep_123")
.eventTypeId("evt_type_123"));
// Several at once
List<Subscription> subs = hookbase.subscriptions().subscribeToMany("app_123", "ep_123",
List.of("evt_type_1", "evt_type_2"));Messages
Messages are the webhooks you send to your customers.
Send Message
SendMessageResponse sent = hookbase.messages().send("app_123", new SendMessageParams()
.eventType("order.created")
.payload(Map.of("orderId", "ord_123"))
.labels(Map.of("region", "eu"))
.idempotencyKey("order-ord_123-created")); // Optional
System.out.println(sent.eventId() + " queued to " + sent.messagesQueued() + " endpoint(s)");Every send carries an idempotency key: the one you set, else one from RequestOptions, else a generated UUID. The API deduplicates on it, so a send retried after a timeout is delivered once.
List Messages and Attempts
for (OutboundMessage m : hookbase.messages().listAll("app_123", new ListMessagesParams().status("failed"))) {
List<MessageAttempt> attempts = hookbase.messages().listAttempts("app_123", m.id());
}Retry Failed Message
// Replays the message (creates a new message)
MessageReplayResult replay = hookbase.messages().retry("app_123", "msg_123");Endpoints
Endpoints are managed by your customers through the Portal, but you can also manage them programmatically.
Create Endpoint
// The signing secret is returned only here; give it to your customer.
Endpoint endpoint = hookbase.endpoints().create("app_123", new CreateEndpointParams()
.url("https://example.com/webhooks")
.description("Production"));
String secret = endpoint.secret();List Endpoints
Page<Endpoint> page = hookbase.endpoints().list("app_123");Get Endpoint Statistics
EndpointStats stats = hookbase.endpoints().getStats("app_123", "ep_123");Rotate the Signing Secret
// The old secret keeps signing for the grace period
EndpointRotateSecretResult rotated = hookbase.endpoints().rotateSecret("app_123", "ep_123",
new RotateEndpointSecretParams().gracePeriodSeconds(3600));Portal Tokens
Portal tokens authenticate the embedded Portal component.
Create Portal Token
PortalToken token = hookbase.portalTokens().create("app_123", new CreatePortalTokenParams()
.name("Customer dashboard")
.scopes(List.of("read", "write"))
.expiresInDays(30));
// Return token.token() to your frontend; it is only returned on create
String plaintext = token.token();Webhook Verification
Help your customers verify incoming webhooks. Always verify the raw bytes of the body: JSON that was parsed and re-serialized no longer matches the signature.
Verify a Request
import app.hookbase.errors.WebhookVerificationException;
import app.hookbase.webhooks.Webhook;
Webhook webhook = new Webhook(System.getenv("HOOKBASE_WEBHOOK_SECRET")); // whsec_...
// Spring Boot example
@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");
}
}verify 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. Pass a Duration to change the tolerance, or a class to bind the payload: webhook.verify(body, headers, OrderEvent.class).
Take the body as byte[], not String: Hookbase sends application/json without a charset, and servlet containers then decode text as ISO-8859-1, which breaks any non-ASCII payload.
Webhook verifies v1 (HMAC-SHA256) signatures with a whsec_ secret. For endpoints using the ed25519 scheme, pass the endpoint's whpk_ public key instead (new Webhook(endpoint.publicKey()), 1.1.0+) and verify checks v1a signatures. See Ed25519 (v1a) Signatures.
Generate Test Headers
Webhook webhook = new Webhook("whsec_dGVzdF9zZWNyZXRfa2V5X2Zvcl90ZXN0aW5n");
String payload = "{\"orderId\":\"test_123\"}";
Map<String, String> headers = webhook.generateTestHeaders(payload);
// Use these headers to test your webhook handlerError Handling
Every failure is a HookbaseException (unchecked). The hierarchy is sealed, so a switch over it can be exhaustive:
| Exception | When |
|---|---|
HookbaseApiException | Any error response: status(), code(), details() |
AuthenticationException | 401 |
ForbiddenException | 403 |
NotFoundException | 404 |
ConflictException | 409 |
ValidationException | 400 / 422, with fieldErrors() |
RateLimitException | 429, with retryAfter() |
HookbaseTimeoutException | The request timed out |
HookbaseNetworkException | Could not connect, or the connection dropped |
WebhookVerificationException | Signature verification failed |
import app.hookbase.errors.*;
try {
hookbase.endpoints().get("app_123", "invalid_id");
} catch (NotFoundException e) {
System.out.println("Endpoint not found");
} catch (RateLimitException e) {
System.out.println("Rate limited, retry after " + e.retryAfter());
} catch (ValidationException e) {
System.out.println("Invalid: " + e.fieldErrors());
} catch (HookbaseApiException e) {
System.out.println("API error: " + e.getMessage() + " (" + e.code() + ")");
}Retries
Failed requests are retried up to maxRetries times (default 3):
- 429 is retried after the
Retry-Afterthe API sends, when it is 60 seconds or less. A 429 without one (a plan or monthly quota) is thrown at once. - 500, 502, 503, 504, timeouts and dropped connections are retried with exponential backoff for every method except POST. A POST is retried only when sending an event (the API deduplicates on its idempotency key), or when you set
RequestOptions.retriesyourself. - A connection that was never made (refused, DNS failure) is always retried.
Request Options
Every method has an overload that takes RequestOptions as its last argument:
import app.hookbase.RequestOptions;
SendMessageResponse sent = hookbase.messages().send("app_123",
new SendMessageParams().eventType("order.created").payload(Map.of()),
RequestOptions.builder()
.timeout(Duration.ofSeconds(60))
.retries(5)
.idempotencyKey("unique_key")
.build());Models
Responses are immutable Java records in app.hookbase.models, named after the API's resources (Application, Endpoint, EventType, Subscription, OutboundMessage, Source, Event, Delivery ...). Field names match the API's JSON. Timestamps are ISO-8601 strings (Instant.parse(app.createdAt())), and free-form JSON such as payloads and metadata is a Jackson JsonNode. Records that carry credentials print them as <redacted> in toString(), so logging one is safe.
The SDK also covers the inbound API (sources, destinations, routes, events, deliveries, transforms, filters, schemas, provider catalog) and admin resources (analytics, API keys, tunnels, cron jobs), following the same pattern. See the README and examples for more.