Skip to content

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:

Terminal window
cd sdks/paylera-sdk-java && mvn -B install -DskipTests
cd ../paylera-spring-boot-starter && mvn -B install -DskipTests
cd ../../examples/java/spring-boot-app && mvn -B test

After 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 receiver

One bean — the identity resolver:

@Configuration
class 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 empty

Each 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:

#RouteForwards to
1POST /api/paylera/attachPOST /v1/attach
2POST /api/paylera/checkPOST /v1/check
3POST /api/paylera/trackPOST /v1/track
4GET /api/paylera/entitlementsGET /v1/subscriptions/{id}/entitlements
5GET /api/paylera/meGET /v1/customers/{id} (auto-create on miss)
6GET /api/paylera/plansGET /v1/plans
7GET /api/paylera/invoicesstub (see starter CHANGELOG)
8POST /api/paylera/billing-portalPOST /v1/customers/{id}/billing-portal-sessions
9POST /api/paylera/subscriptions/{id}/upgradePOST /v1/subscriptions/{id}/change-plan
10POST /api/paylera/subscriptions/{id}/cancelPOST /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 protection

The 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:

@Configuration
class 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:

@Bean
PayleraIdentifier 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:

@Bean
PayleraClient 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.