Java SDK — Spring Boot starter
dev.paylera:paylera-spring-boot-starter is a Spring Boot 3
auto-configuration on top of the typed
paylera-sdk client. The starter ships
two opinionated pieces a merchant backend needs to host the React SDK
and receive Paylera webhooks:
- Relay controller at
/api/paylera/**— implements the ten cross-language relay routes. - Webhook receiver at
/webhooks/paylera— verifies signatures and dispatches to@PayleraWebhookHandler-annotated methods.
Both pieces are independently opt-out: configure only what you need.
Install
<dependency> <groupId>dev.paylera</groupId> <artifactId>paylera-spring-boot-starter</artifactId> <version>0.0.0-SNAPSHOT</version></dependency>The starter declares dev.paylera:paylera-sdk as a transitive
dependency — no separate paylera-sdk dep needed.
Local dev loop
Until the artefacts publish to Maven Central the dependency resolves
through your local Maven repository (~/.m2/repository/). Install the
SDK and starter in order:
cd sdks/paylera-sdk-java && mvn -B install -DskipTestscd ../paylera-spring-boot-starter && mvn -B install -DskipTestscd ../../examples/java/spring-boot-app && mvn -B testAfter editing the SDK or starter, re-run mvn -B install -DskipTests
in the touched module before the example picks up the change.
Local-Maven snapshots don’t auto-refresh from target/.
Minimum wiring
application.yaml:
paylera: api-token: ${PAYLERA_API_TOKEN} # required webhook-secret: ${PAYLERA_WEBHOOK_SECRET} # enables the webhook receiverOne bean — the identity resolver:
@Configurationclass PayleraConfig { @Bean PayleraIdentifier identifier() { return request -> PayleraCustomerIdentity.builder() .customerId(SecurityContext.user().getPayleraCustomerId()) .email(SecurityContext.user().getEmail()) .name(SecurityContext.user().getName()) .build(); }}That’s the whole setup. The relay controller and webhook controller are auto-registered.
The starter refuses to mount the relay controller until a
PayleraIdentifier bean exists. Without an identifier the relay can’t
authenticate inbound requests — the autoconfig leaves the relay
un-mounted so the host app boots cleanly even during partial
provisioning.
All configuration knobs
paylera: api-token: ${PAYLERA_API_TOKEN} webhook-secret: ${PAYLERA_WEBHOOK_SECRET}
base-url: "" # optional — default routes by token prefix api-version: "2026-05-01" # optional — default DEFAULT_API_VERSION relay-prefix: /api/paylera # optional — default /api/paylera webhook-prefix: /webhooks/paylera # optional — default /webhooks/paylera auto-create-customer: true # optional — default true csrf: true # optional — default true webhook-tolerance-seconds: 300 # optional — default 300 accepted-secrets: [] # optional — rotation list, default emptyEach binds to a field on
dev.paylera.starter.PayleraProperties — the javadoc on that class
documents the contract.
Relay routes
The starter exposes exactly the ten cross-language relay routes:
| # | Route | Forwards to |
|---|---|---|
| 1 | POST /api/paylera/attach | POST /v1/attach |
| 2 | POST /api/paylera/check | POST /v1/check |
| 3 | POST /api/paylera/track | POST /v1/track |
| 4 | GET /api/paylera/entitlements | GET /v1/subscriptions/{id}/entitlements |
| 5 | GET /api/paylera/me | GET /v1/customers/{id} (auto-create on miss) |
| 6 | GET /api/paylera/plans | GET /v1/plans |
| 7 | GET /api/paylera/invoices | stub (see starter CHANGELOG) |
| 8 | POST /api/paylera/billing-portal | POST /v1/customers/{id}/billing-portal-sessions |
| 9 | POST /api/paylera/subscriptions/{id}/upgrade | POST /v1/subscriptions/{id}/change-plan |
| 10 | POST /api/paylera/subscriptions/{id}/cancel | POST /v1/subscriptions/{id}/cancel |
Plus GET /api/paylera/csrf-token for the React SDK’s double-submit
cookie pattern.
What the starter handles for you
CSRF
Double-submit cookie on every state-changing route. Opt out with
paylera.csrf: false when you chain your own CSRF middleware:
paylera: csrf: false # the relay then trusts the host framework's protectionThe token mint lives at GET /api/paylera/csrf-token. The React SDK
provider calls this on mount and replays the token in the
X-Paylera-CSRF-Token header.
Idempotency
Idempotency-Key auto-stamped on every state-changing relay call.
Caller-supplied keys (or the React SDK’s dedupKey) take precedence.
Auto-create customer
When PayleraIdentifier#identify returns a customer-identity with a
null customerId and paylera.auto-create-customer: true (default),
the relay calls POST /v1/customers with the identify metadata,
caches the resulting id per request, and proceeds.
The idempotency key for the auto-create call is
paylera-relay-autocreate:<email> so concurrent first-time requests
converge on a single Paylera customer record.
Subscription-ownership verification
Before forwarding upgrade / cancel, the relay verifies the subscription belongs to the authenticated customer. The React app is not trusted to scope this — only the relay sees the secret token.
RFC 9457 pass-through
Upstream errors are forwarded verbatim — status code preserved,
Content-Type: application/problem+json body intact. No information
loss in the relay layer.
OpenTelemetry
Spans under Paylera.Relay.<Op> and Paylera.Webhook.<event>. See
OpenTelemetry.
@PayleraWebhookHandler
Annotate a method on any Spring bean to register a webhook handler:
@Configurationclass PayleraConfig {
@PayleraWebhookHandler("invoice.paid") public void onInvoicePaid(InvoicePaidEvent event) { // event.invoiceId(), event.customerId(), event.totalAmount(), ... }
@PayleraWebhookHandler("subscription.canceled") public void onSubscriptionCanceled(SubscriptionCanceledEvent event) { // ... }}At startup the dispatcher discovers every annotated method, validates
the signature (single-arg accepting PayleraWebhookEvent or one of
its typed subclasses), and routes matching deliveries to it.
A return value of void (or a normal return on a non-void method) is
a 200 — “you got it, don’t resend.” A thrown exception is a 500 —
which is what triggers Paylera’s retry schedule. Bad signatures are
401, never retried.
See Webhooks for the full event catalog mapping and signature contract.
Multiple controllers per request
The relay calls PayleraIdentifier#identify once per inbound request,
after CSRF validation succeeds. Returning null from identify()
denies the request (the relay responds 401). Returning an identity
with a null customerId is fine and triggers auto-create when
enabled.
A real-world identifier pulls from Spring Security:
@BeanPayleraIdentifier identifier() { return request -> { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth == null || !auth.isAuthenticated()) { return null; // → 401 from the relay } var user = (PayleraAwarePrincipal) auth.getPrincipal(); return PayleraCustomerIdentity.builder() .customerId(user.getPayleraCustomerId()) // null OK if first touch .email(user.getEmail()) .name(user.getDisplayName()) .currency("USD") .build(); };}Bringing your own PayleraClient
The autoconfig builds a PayleraClient from
paylera.api-token + paylera.base-url + paylera.api-version. To
swap in your own (custom OkHttpClient, custom tracer, …), declare
the bean and the autoconfig backs off:
@BeanPayleraClient payleraClient(PayleraProperties props) { return PayleraClient.builder() .apiToken(props.getApiToken()) .tracer(myTracer) .build();}The relay controller, webhook dispatcher, and CSRF / idempotency filters all consume this bean — they’re agnostic to who built it.
Example app
A working example lives at
examples/java/spring-boot-app/:
flat-POM Spring Boot 3 app with one @Bean PayleraIdentifier, one
@PayleraWebhookHandler("invoice.paid") method, and a smoke test that
proves the relay + webhook receiver mount cleanly.