Java SDK — Direct typed client
dev.paylera:paylera-sdk is the typed HTTP client at the bottom of
the Java SDK stack. It depends on nothing Spring-specific — use it
from Quarkus, Micronaut, Vert.x, a plain main(), or an AWS Lambda
handler.
The Spring Boot starter sits on top of this client; if you’re on Spring Boot 3 the starter is the ergonomic path. Reach for the typed client directly when:
- You’re not on Spring Boot.
- You want full control over the
OkHttpClient(connection pool, interceptors, proxy config). - You’re embedding the SDK in a CLI tool, scheduled job, or other short-lived process.
- You only need server-to-server calls, not the merchant-backend relay.
Install
<dependency> <groupId>dev.paylera</groupId> <artifactId>paylera-sdk</artifactId> <version>0.0.0-SNAPSHOT</version></dependency>The SDK depends on okhttp, gson, commons-lang3, and (optionally)
opentelemetry-api. No reflection-heavy frameworks pulled in
transitively.
Build a client
import dev.paylera.sdk.PayleraClient;
var client = PayleraClient.builder() .apiToken(System.getenv("PAYLERA_API_TOKEN")) .build();PayleraClient is immutable and thread-safe. Build it once, reuse it
across the lifetime of the JVM. Multiple instances pointing at
different tokens / base URLs run side-by-side without conflict — no
global state.
Builder options
var client = PayleraClient.builder() .apiToken(token) .baseUrl("https://api.test.paylera.dev") // override token-prefix routing .apiVersion("2026-05-01") // override DEFAULT_API_VERSION .tracer(GlobalOpenTelemetry.getTracer("Paylera.Sdk")) .build();| Method | Default |
|---|---|
apiToken(String) | (required) |
baseUrl(String) | Routed from token prefix — pl_live_* → api.paylera.dev, pl_test_* → api.test.paylera.dev |
apiVersion(String) | "2026-05-01" (PayleraClient.DEFAULT_API_VERSION) |
tracer(io.opentelemetry.api.trace.Tracer) | no-op tracer |
Make a call
import dev.paylera.sdk.model.CheckRequest;import dev.paylera.sdk.model.CheckResponse;
CheckResponse resp = client.check(new CheckRequest() .customerId(customerId) .featureCode("api_calls") .requiredUsage("1"));
if (Boolean.TRUE.equals(resp.getAllowed())) { // gate passed}Every operation has a hand-written wrapper on PayleraClient. The
wrappers handle base-URL routing, Paylera-Api-Version stamping,
Idempotency-Key auto-generation, retries, exception translation,
and tracing.
The full surface:
client.createCustomer(...)client.listCustomers(...)client.getCustomer(UUID)
client.getPlan(UUID)client.listPlans(...)
client.getSubscription(UUID)client.listSubscriptions(...)client.cancelSubscription(UUID, body)client.cancelSubscriptionImmediate(UUID, version, body)client.pauseSubscription(UUID)client.resumeSubscription(UUID)client.changeSubscriptionPlan(UUID, body)client.resolveSubscriptionEntitlements(UUID, asOf)
client.attach(body)client.check(body)client.checkByFeatureCode(featureCode, customerId, subscriptionId)client.track(body)
client.createBillingPortalSession(UUID, [body])Need an operation the wrapper doesn’t expose ergonomically? Drop down
to client.rawApi() — that returns the generated
dev.paylera.sdk.api.DefaultApi with every operation in the spec.
Idempotency
Operations that require an Idempotency-Key on the wire get a fresh
UUID v7 (time-ordered, RFC 9562) stamped automatically. Pass an
explicit key to share it across your own retry budget:
client.track( new TrackRequest().customerId(id).featureCode("api_calls").value("1"), "my-stable-dedup-key", /* xRequestId */ null);UUID v7 keeps the server-side idempotency store sortable by mint time — eviction is O(log n) rather than O(n).
Typed exceptions
Every 4xx/5xx is translated into a typed subclass of
PayleraException based on the HTTP status + the code field in the
RFC 9457 problem-details body:
| Subclass | When |
|---|---|
PayleraAuthenticationException | HTTP 401 |
PayleraAuthorizationException | HTTP 403 |
PayleraNotFoundException | HTTP 404 |
PayleraIdempotencyConflictException | 409 + code = paylera.idempotency_conflict |
PayleraIdempotencyInProgressException | 409 + code = paylera.idempotency_in_progress |
PayleraPreconditionFailedException | HTTP 412 (optimistic-concurrency version mismatch) |
PayleraRateLimitException | HTTP 429 — carries Duration retryAfter |
PayleraServerException | HTTP 5xx + transport-level failures |
PayleraException (base) | any other 4xx |
Every exception exposes:
e.getCode() // e.g. "paylera.invoice_not_found"e.getDetail() // human-readable detail stringe.getRequestId() // Paylera-Request-Id for support ticketse.isRetryable()e.getErrors() // field-scoped list parsed from the problem-details bodyTypical handling:
try { client.check(req);} catch (PayleraRateLimitException e) { sleep(e.retryAfter()); return retry();} catch (PayleraAuthenticationException e) { // token revoked / rotated — pull a fresh secret throw e;} catch (PayleraException e) { logger.warn("paylera error code={} req={} detail={}", e.getCode(), e.getRequestId(), e.getDetail()); throw e;}Retries
Built-in retry on 5xx + 429:
- 5 total attempts (1 initial + 4 retries)
- base 200 ms, doubled per attempt, capped at 8 s
- full-jitter scale on each delay
Retry-Afterheader honoured on 429 when longer than the computed backoff- 4xx responses never retry — replaying a malformed request wastes quota and risks duplicate writes against non-idempotent endpoints
Retries are stamped with the same Idempotency-Key as the original
request, so duplicate-write protection holds end-to-end.
Pagination
list* responses carry a cursor field; loop until null:
String cursor = null;do { var page = client.listSubscriptions(customerId, null, null, 100, cursor); page.getData().forEach(sub -> { // ... }); cursor = page.getNextCursor();} while (cursor != null);A page-iterator helper is on the roadmap; for now the cursor loop is the canonical pattern.
Closing the client
PayleraClient doesn’t expose close() — the underlying
OkHttpClient connection pool is cleaned up by the JVM at exit. In a
short-lived process (CLI, Lambda) you don’t need to do anything; in a
long-lived process you only need to close the client if you’ve
constructed a custom OkHttpClient with a custom dispatcher that
you’ve registered for shutdown elsewhere.
Quarkus / Micronaut / etc.
Wire PayleraClient as a singleton bean in your framework’s DI
container:
// Quarkus@ApplicationScopedclass PayleraClientProducer {
@Produces @Singleton PayleraClient payleraClient(@ConfigProperty(name = "paylera.api-token") String token) { return PayleraClient.builder().apiToken(token).build(); }}// Micronaut@Factoryclass PayleraClientFactory {
@Bean @Singleton PayleraClient payleraClient(@Value("${paylera.api-token}") String token) { return PayleraClient.builder().apiToken(token).build(); }}The webhook receiver and relay controller from the Spring starter are
Spring-specific. For non-Spring frameworks you implement the receiver
in your framework’s idiom and use
dev.paylera.starter.PayleraWebhookVerifier (no Spring deps) to
verify signatures, or write your own HMAC check against the
protocol contract.
See also
- Getting started — the SDK at a glance.
- Spring Boot starter — for Spring Boot 3 hosts.
- OpenTelemetry — wiring spans.