diff --git a/with-java-springboot/.env.example b/with-java-springboot/.env.example
new file mode 100644
index 0000000..623c426
--- /dev/null
+++ b/with-java-springboot/.env.example
@@ -0,0 +1,8 @@
+# https://docs.polar.sh/integrate/oat
+POLAR_ACCESS_TOKEN="polar_oat_..."
+# https://docs.polar.sh/integrate/webhooks/endpoints#setup-webhooks
+POLAR_WEBHOOK_SECRET="polar_whs_..."
+# Polar server mode - production or sandbox
+POLAR_MODE="sandbox"
+# client url - this is the URL the customer would be led to if they purchase something.
+POLAR_SUCCESS_URL="http://localhost:8080"
diff --git a/with-java-springboot/.gitignore b/with-java-springboot/.gitignore
new file mode 100644
index 0000000..5f5ade2
--- /dev/null
+++ b/with-java-springboot/.gitignore
@@ -0,0 +1,8 @@
+/target/
+/.env
+/.idea/
+/*.iml
+/.vscode/
+/node_modules/
+.DS_Store
+/.mvn/
\ No newline at end of file
diff --git a/with-java-springboot/.mvn/wrapper/maven-wrapper.properties b/with-java-springboot/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..7c6b218
--- /dev/null
+++ b/with-java-springboot/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
diff --git a/with-java-springboot/README.md b/with-java-springboot/README.md
new file mode 100644
index 0000000..bf3a7b1
--- /dev/null
+++ b/with-java-springboot/README.md
@@ -0,0 +1,29 @@
+
+
+# Getting started with Polar and Java Spring Boot
+
+## Clone the repository
+
+```bash
+npx degit polarsource/examples/with-java-springboot ./with-java-springboot
+```
+
+## How to use
+
+1. Run the command below to copy the `.env.example` file:
+
+```bash
+cp .env.example .env
+```
+
+2. Run the command below to install project dependencies:
+
+```bash
+mvn clean install
+```
+
+3. Run the Spring Boot application using the following command:
+
+```bash
+mvn spring-boot:run
+```
diff --git a/with-java-springboot/pom.xml b/with-java-springboot/pom.xml
new file mode 100644
index 0000000..dcf459e
--- /dev/null
+++ b/with-java-springboot/pom.xml
@@ -0,0 +1,67 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.4.1
+
+
+ com.example
+ polar-springboot-example
+ 0.0.1-SNAPSHOT
+ polar-springboot-example
+ Polar with Spring Boot Example
+
+ 17
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ io.github.cdimascio
+ dotenv-java
+ 3.1.0
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
diff --git a/with-java-springboot/src/main/java/com/example/polar/PolarApplication.java b/with-java-springboot/src/main/java/com/example/polar/PolarApplication.java
new file mode 100644
index 0000000..cd20d06
--- /dev/null
+++ b/with-java-springboot/src/main/java/com/example/polar/PolarApplication.java
@@ -0,0 +1,21 @@
+package com.example.polar;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+public class PolarApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(PolarApplication.class, args);
+ }
+
+ @Bean
+ public Dotenv dotenv() {
+ return Dotenv.configure()
+ .ignoreIfMissing()
+ .load();
+ }
+}
diff --git a/with-java-springboot/src/main/java/com/example/polar/controller/PolarController.java b/with-java-springboot/src/main/java/com/example/polar/controller/PolarController.java
new file mode 100644
index 0000000..7f01303
--- /dev/null
+++ b/with-java-springboot/src/main/java/com/example/polar/controller/PolarController.java
@@ -0,0 +1,118 @@
+package com.example.polar.controller;
+
+import com.example.polar.service.PolarService;
+import com.example.polar.util.WebhookVerifier;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.github.cdimascio.dotenv.Dotenv;
+import org.springframework.web.servlet.view.RedirectView;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.*;
+
+@Controller
+public class PolarController {
+
+ private final PolarService polarService;
+ private final String webhookSecret;
+ private final String successUrl;
+
+ public PolarController(PolarService polarService, Dotenv dotenv) {
+ this.polarService = polarService;
+ this.webhookSecret = dotenv.get("POLAR_WEBHOOK_SECRET");
+ this.successUrl = dotenv.get("POLAR_SUCCESS_URL");
+ }
+
+ @GetMapping("/")
+ @ResponseBody
+ public String home() {
+ try {
+ JsonNode products = polarService.listProducts();
+ StringBuilder html = new StringBuilder();
+ html.append("
")
+ .append("");
+
+ if (products != null) {
+ JsonNode items = products.get("items");
+ if (items != null && items.isArray()) {
+ for (JsonNode item : items) {
+ String id = item.get("id").asText();
+ String name = item.get("name").asText();
+ html.append("");
+ }
+ }
+ }
+
+ html.append("");
+ return html.toString();
+ } catch (Exception e) {
+ return "Error: " + e.getMessage() + "";
+ }
+ }
+
+ @GetMapping("/checkout")
+ public Object checkout(@RequestParam(name = "products") String products, HttpServletRequest request) {
+ try {
+ String productId = products;
+ String host = request.getHeader("host");
+ String effectiveSuccessUrl = (successUrl != null && !successUrl.isEmpty())
+ ? successUrl
+ : "http://" + host + "/";
+
+ JsonNode checkout = polarService.createCheckout(productId, effectiveSuccessUrl);
+ if (checkout == null || !checkout.has("url")) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body("Failed to create checkout session");
+ }
+ return new RedirectView(checkout.get("url").asText());
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
+ }
+ }
+
+ @GetMapping("/portal")
+ public Object portal(@RequestParam String email) {
+ try {
+ JsonNode customers = polarService.findCustomerByEmail(email);
+ if (customers == null || !customers.has("items")) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Customer not found or API error");
+ }
+
+ JsonNode items = customers.get("items");
+ if (items == null || !items.isArray() || items.size() == 0) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Customer not found");
+ }
+
+ String customerId = items.get(0).get("id").asText();
+ JsonNode session = polarService.createCustomerSession(customerId);
+ if (session == null || !session.has("customer_portal_url")) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body("Failed to create customer session");
+ }
+ return new RedirectView(session.get("customer_portal_url").asText());
+ } catch (Exception e) {
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error: " + e.getMessage());
+ }
+ }
+
+ @PostMapping("/polar/webhooks")
+ @ResponseBody
+ public ResponseEntity webhooks(@RequestBody String body, HttpServletRequest request) {
+ String id = request.getHeader("webhook-id");
+ String timestamp = request.getHeader("webhook-timestamp");
+ String signature = request.getHeader("webhook-signature");
+
+ boolean isValid = WebhookVerifier.verify(body, id, timestamp, signature, webhookSecret);
+ if (!isValid) {
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid signature");
+ }
+
+ System.out.println("Received webhook: " + body);
+ return ResponseEntity.ok(body);
+ }
+}
diff --git a/with-java-springboot/src/main/java/com/example/polar/service/PolarService.java b/with-java-springboot/src/main/java/com/example/polar/service/PolarService.java
new file mode 100644
index 0000000..9a4629c
--- /dev/null
+++ b/with-java-springboot/src/main/java/com/example/polar/service/PolarService.java
@@ -0,0 +1,92 @@
+package com.example.polar.service;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestClient;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.util.Map;
+
+@Service
+public class PolarService {
+
+ private final RestClient restClient;
+ private final String serverUrl;
+
+ public PolarService(Dotenv dotenv) {
+ String accessToken = dotenv.get("POLAR_ACCESS_TOKEN");
+ String mode = dotenv.get("POLAR_MODE", "production");
+
+ System.out.println("PolarService initialized with Mode: " + mode);
+ if (accessToken == null || accessToken.isEmpty()) {
+ System.err.println("CRITICAL: POLAR_ACCESS_TOKEN is missing!");
+ } else {
+ System.out.println("POLAR_ACCESS_TOKEN found (starts with: "
+ + accessToken.substring(0, Math.min(10, accessToken.length())) + "...)");
+ }
+
+ this.serverUrl = mode.equals("sandbox")
+ ? "https://sandbox-api.polar.sh"
+ : "https://api.polar.sh";
+
+ this.restClient = RestClient.builder()
+ .baseUrl(this.serverUrl)
+ .defaultHeader("Authorization", "Bearer " + accessToken)
+ .defaultHeader("Content-Type", "application/json")
+ .build();
+ }
+
+ public JsonNode listProducts() {
+ try {
+ return restClient.get()
+ .uri("/v1/products/?is_archived=false")
+ .retrieve()
+ .body(JsonNode.class);
+ } catch (Exception e) {
+ System.err.println("Error in listProducts: " + e.getMessage());
+ return null;
+ }
+ }
+
+ public JsonNode createCheckout(String productId, String successUrl) {
+ try {
+ return restClient.post()
+ .uri("/v1/checkouts/")
+ .body(Map.of(
+ "products", new String[] { productId },
+ "success_url", successUrl))
+ .retrieve()
+ .body(JsonNode.class);
+ } catch (Exception e) {
+ System.err.println("Error in createCheckout: " + e.getMessage());
+ return null;
+ }
+ }
+
+ public JsonNode findCustomerByEmail(String email) {
+ try {
+ return restClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/v1/customers/")
+ .queryParam("email", email)
+ .build())
+ .retrieve()
+ .body(JsonNode.class);
+ } catch (Exception e) {
+ System.err.println("Error in findCustomerByEmail: " + e.getMessage());
+ return null;
+ }
+ }
+
+ public JsonNode createCustomerSession(String customerId) {
+ try {
+ return restClient.post()
+ .uri("/v1/customer-sessions/")
+ .body(Map.of("customer_id", customerId))
+ .retrieve()
+ .body(JsonNode.class);
+ } catch (Exception e) {
+ System.err.println("Error in createCustomerSession: " + e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/with-java-springboot/src/main/java/com/example/polar/util/WebhookVerifier.java b/with-java-springboot/src/main/java/com/example/polar/util/WebhookVerifier.java
new file mode 100644
index 0000000..ab7c32d
--- /dev/null
+++ b/with-java-springboot/src/main/java/com/example/polar/util/WebhookVerifier.java
@@ -0,0 +1,47 @@
+package com.example.polar.util;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+public class WebhookVerifier {
+
+ private static final String HMAC_SHA256 = "HmacSHA256";
+
+ public static boolean verify(String body, String id, String timestamp, String signature, String secret) {
+ if (body == null || id == null || timestamp == null || signature == null || secret == null) {
+ return false;
+ }
+
+ try {
+ // parse signature
+ String[] signatures = signature.split(" ");
+ String expectedSignature = "";
+ for (String s : signatures) {
+ if (s.startsWith("v1,")) {
+ expectedSignature = s.substring(3);
+ break;
+ }
+ }
+
+ if (expectedSignature.isEmpty()) {
+ return false;
+ }
+
+ // signed payload
+ String signedPayload = id + "." + timestamp + "." + body;
+
+ Mac sha256Hmac = Mac.getInstance(HMAC_SHA256);
+ SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
+ sha256Hmac.init(secretKey);
+
+ byte[] hash = sha256Hmac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
+ String actualSignature = Base64.getEncoder().encodeToString(hash);
+
+ return actualSignature.equals(expectedSignature);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+}