google-site-verification: google61fe8ba583a51912.html
Secure Payment Processing: Integrating PayPal SDK with Java Applications


Article

In e-commerce and digital services, secure payment processing is fundamental. PayPal provides one of the world's most trusted payment platforms, and its Java SDK enables seamless integration of payment capabilities into Java applications. From simple buy buttons to complex subscription management, the PayPal SDK empowers Java developers to implement robust payment solutions while handling security, compliance, and global payment methods.

What is the PayPal Java SDK?

The PayPal Java SDK provides programmatic access to PayPal's REST APIs, enabling:

  • Payment Processing: Accept credit cards, PayPal, and alternative payment methods
  • Subscription Management: Recurring billing and subscription workflows
  • Payouts: Send mass payments to multiple recipients
  • Order Management: Create, capture, and manage orders
  • Webhook Handling: Receive real-time payment notifications
  • Dispute Management: Handle chargebacks and disputes

Why Use PayPal SDK in Java Applications?

  1. Security: PCI DSS compliant, tokenized payments, reduced liability
  2. Global Reach: Support for 200+ markets, 100+ currencies
  3. User Trust: Recognized and trusted payment brand
  4. Developer Experience: Well-documented SDK with clear APIs
  5. Compliance: Handles regulatory requirements automatically

PayPal Integration Architecture

Java Application → PayPal Java SDK → PayPal REST API → Payment Processing ↓ Webhooks → Notification Handling ↓ Database → Payment Record Storage

Setting Up PayPal SDK

1. Add Dependencies:

<!-- pom.xml --> <dependencies> <!-- PayPal Core SDK --> <dependency> <groupId>com.paypal.sdk</groupId> <artifactId>rest-api-sdk</artifactId> <version>1.14.0</version> </dependency> <!-- Spring Boot Starters --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>

2. Configuration Properties:

# application.yml paypal: mode: ${PAYPAL_MODE:sandbox} # sandbox or live client-id: ${PAYPAL_CLIENT_ID:your-client-id} client-secret: ${PAYPAL_CLIENT_SECRET:your-client-secret} webhook-id: ${PAYPAL_WEBHOOK_ID:your-webhook-id} # Timeout settings connection-timeout: 30000 read-timeout: 60000 # Logging log-level: INFO app: base-url: ${APP_BASE_URL:http://localhost:8080} frontend-url: ${FRONTEND_URL:http://localhost:3000}

3. PayPal Configuration Class:

@Configuration @ConfigurationProperties(prefix = "paypal") @Data public class PayPalConfig { private String mode; private String clientId; private String clientSecret; private String webhookId; private Integer connectionTimeout; private Integer readTimeout; private String logLevel; @Bean public APIContext apiContext() { APIContext context = new APIContext(clientId, clientSecret, mode); context.setConfigurationMap(configurationMap()); return context; } @Bean public Map<String, String> configurationMap() { Map<String, String> config = new HashMap<>(); config.put("mode", mode); config.put("http.ConnectionTimeOut", String.valueOf(connectionTimeout)); config.put("http.Retry", "3"); config.put("http.ReadTimeOut", String.valueOf(readTimeout)); config.put("http.MaxConnection", "100"); config.put("log.LogEnabled", "true"); config.put("log.FileName", "paypal.log"); config.put("log.LogLevel", logLevel); config.put("log.LoggerLevel", logLevel); return config; } }

Core Payment Service

1. PayPal Payment Service:

@Service @Slf4j public class PayPalPaymentService { private final APIContext apiContext; private final PaymentRepository paymentRepository; private final PayPalConfig payPalConfig; public PayPalPaymentService(APIContext apiContext, PaymentRepository paymentRepository, PayPalConfig payPalConfig) { this.apiContext = apiContext; this.paymentRepository = paymentRepository; this.payPalConfig = payPalConfig; } public Payment createPayment(CreatePaymentRequest request) throws PayPalRESTException { try { // Set payment amount Amount amount = new Amount(); amount.setCurrency(request.getCurrency()); amount.setTotal(String.format("%.2f", request.getAmount())); // Set transaction Transaction transaction = new Transaction(); transaction.setAmount(amount); transaction.setDescription(request.getDescription()); transaction.setCustom(request.getOrderId()); // Set item list if provided if (request.getItems() != null && !request.getItems().isEmpty()) { transaction.setItemList(createItemList(request.getItems())); } // Set payment details List<Transaction> transactions = new ArrayList<>(); transactions.add(transaction); // Set payer Payer payer = new Payer(); payer.setPaymentMethod(request.getPaymentMethod().toString()); // Set redirect URLs RedirectUrls redirectUrls = new RedirectUrls(); redirectUrls.setCancelUrl(request.getCancelUrl()); redirectUrls.setReturnUrl(request.getReturnUrl()); // Create payment Payment payment = new Payment(); payment.setIntent("sale"); payment.setPayer(payer); payment.setTransactions(transactions); payment.setRedirectUrls(redirectUrls); // Create payment in PayPal Payment createdPayment = payment.create(apiContext); // Save payment record to database savePaymentRecord(createdPayment, request); log.info("Created PayPal payment: {}", createdPayment.getId()); return createdPayment; } catch (PayPalRESTException e) { log.error("Failed to create PayPal payment", e); throw e; } } public Payment executePayment(String paymentId, String payerId) throws PayPalRESTException { try { Payment payment = new Payment(); payment.setId(paymentId); PaymentExecution paymentExecution = new PaymentExecution(); paymentExecution.setPayerId(payerId); Payment executedPayment = payment.execute(apiContext, paymentExecution); // Update payment record in database updatePaymentRecord(executedPayment); log.info("Executed PayPal payment: {}", paymentId); return executedPayment; } catch (PayPalRESTException e) { log.error("Failed to execute PayPal payment: {}", paymentId, e); throw e; } } public Payment getPayment(String paymentId) throws PayPalRESTException { try { return Payment.get(apiContext, paymentId); } catch (PayPalRESTException e) { log.error("Failed to get PayPal payment: {}", paymentId, e); throw e; } } public Refund refundPayment(String paymentId, RefundRequest request) throws PayPalRESTException { try { // Get the sale ID from the payment Payment payment = getPayment(paymentId); String saleId = extractSaleId(payment); // Create refund Amount amount = new Amount(); amount.setCurrency(request.getCurrency()); amount.setTotal(String.format("%.2f", request.getAmount())); Refund refund = new Refund(); refund.setAmount(amount); refund.setDescription(request.getReason()); // Execute refund Sale sale = new Sale(); sale.setId(saleId); Refund executedRefund = sale.refund(apiContext, refund); // Save refund record saveRefundRecord(executedRefund, paymentId); log.info("Refunded PayPal payment: {}", paymentId); return executedRefund; } catch (PayPalRESTException e) { log.error("Failed to refund PayPal payment: {}", paymentId, e); throw e; } } private ItemList createItemList(List<PaymentItem> items) { List<Item> paypalItems = items.stream() .map(this::convertToPayPalItem) .collect(Collectors.toList()); ItemList itemList = new ItemList(); itemList.setItems(paypalItems); return itemList; } private Item convertToPayPalItem(PaymentItem item) { Item paypalItem = new Item(); paypalItem.setName(item.getName()); paypalItem.setCurrency(item.getCurrency()); paypalItem.setPrice(String.format("%.2f", item.getPrice())); paypalItem.setQuantity(String.valueOf(item.getQuantity())); paypalItem.setSku(item.getSku()); return paypalItem; } private String extractSaleId(Payment payment) { return payment.getTransactions().stream() .flatMap(t -> t.getRelatedResources().stream()) .map(RelatedResources::getSale) .filter(Objects::nonNull) .map(Sale::getId) .findFirst() .orElseThrow(() -> new RuntimeException("Sale not found for payment: " + payment.getId())); } private void savePaymentRecord(Payment payment, CreatePaymentRequest request) { PaymentRecord record = PaymentRecord.builder() .paymentId(payment.getId()) .intent(payment.getIntent()) .state(payment.getState()) .currency(request.getCurrency()) .amount(request.getAmount()) .orderId(request.getOrderId()) .description(request.getDescription()) .createdAt(Instant.now()) .build(); paymentRepository.save(record); } private void updatePaymentRecord(Payment payment) { paymentRepository.findByPaymentId(payment.getId()) .ifPresent(record -> { record.setState(payment.getState()); record.setUpdatedAt(Instant.now()); paymentRepository.save(record); }); } private void saveRefundRecord(Refund refund, String paymentId) { // Implementation for saving refund records } }

Data Models

1. Request/Response Models:

@Data @Builder @NoArgsConstructor @AllArgsConstructor public class CreatePaymentRequest { @NotNull private Double amount; @NotBlank private String currency; @NotBlank private String description; @NotBlank private String orderId; @NotNull private PaymentMethod paymentMethod; @NotBlank private String returnUrl; @NotBlank private String cancelUrl; private List<PaymentItem> items; public enum PaymentMethod { PAYPAL, CREDIT_CARD } } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PaymentItem { private String name; private String currency; private Double price; private Integer quantity; private String sku; private String description; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PaymentResponse { private String paymentId; private String state; private String approvalUrl; private Instant createdAt; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class ExecutePaymentRequest { @NotBlank private String paymentId; @NotBlank private String payerId; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RefundRequest { @NotBlank private String paymentId; @NotNull private Double amount; @NotBlank private String currency; private String reason; }

2. Database Entities:

@Entity @Table(name = "payment_records") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class PaymentRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "payment_id", unique = true, nullable = false) private String paymentId; @Column(name = "intent") private String intent; @Column(name = "state") private String state; @Column(name = "currency") private String currency; @Column(name = "amount") private Double amount; @Column(name = "order_id") private String orderId; @Column(name = "description") private String description; @Column(name = "created_at") private Instant createdAt; @Column(name = "updated_at") private Instant updatedAt; @PrePersist protected void onCreate() { createdAt = Instant.now(); } @PreUpdate protected void onUpdate() { updatedAt = Instant.now(); } } @Entity @Table(name = "refund_records") @Data @Builder @NoArgsConstructor @AllArgsConstructor public class RefundRecord { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "refund_id") private String refundId; @Column(name = "payment_id") private String paymentId; @Column(name = "amount") private Double amount; @Column(name = "currency") private String currency; @Column(name = "state") private String state; @Column(name = "reason") private String reason; @Column(name = "created_at") private Instant createdAt; }

Subscription Management

1. Subscription Service:

@Service @Slf4j public class PayPalSubscriptionService { private final APIContext apiContext; private final SubscriptionRepository subscriptionRepository; public PayPalSubscriptionService(APIContext apiContext, SubscriptionRepository subscriptionRepository) { this.apiContext = apiContext; this.subscriptionRepository = subscriptionRepository; } public Plan createBillingPlan(CreatePlanRequest request) throws PayPalRESTException { try { // Create plan Plan plan = new Plan(); plan.setName(request.getName()); plan.setDescription(request.getDescription()); plan.setType("INFINITE"); // or "FIXED" // Set payment definitions List<PaymentDefinition> paymentDefinitions = new ArrayList<>(); PaymentDefinition paymentDefinition = new PaymentDefinition(); paymentDefinition.setName("Regular Payments"); paymentDefinition.setType("REGULAR"); paymentDefinition.setFrequency(request.getFrequency().toString()); paymentDefinition.setFrequencyInterval(String.valueOf(request.getFrequencyInterval())); paymentDefinition.setCycles(String.valueOf(request.getCycles())); // Set amount Currency currency = new Currency(); currency.setCurrency(request.getCurrency()); currency.setValue(String.format("%.2f", request.getAmount())); paymentDefinition.setAmount(currency); paymentDefinitions.add(paymentDefinition); plan.setPaymentDefinitions(paymentDefinitions); // Set merchant preferences MerchantPreferences merchantPreferences = new MerchantPreferences(); merchantPreferences.setSetupFee(currency); merchantPreferences.setCancelUrl(request.getCancelUrl()); merchantPreferences.setReturnUrl(request.getReturnUrl()); merchantPreferences.setAutoBillAmount("YES"); merchantPreferences.setInitialFailAmountAction("CONTINUE"); plan.setMerchantPreferences(merchantPreferences); // Create plan Plan createdPlan = plan.create(apiContext); createdPlan = updatePlanStatus(createdPlan.getId(), "ACTIVE"); log.info("Created billing plan: {}", createdPlan.getId()); return createdPlan; } catch (PayPalRESTException e) { log.error("Failed to create billing plan", e); throw e; } } public Agreement createBillingAgreement(CreateAgreementRequest request) throws PayPalRESTException { try { // Create agreement Agreement agreement = new Agreement(); agreement.setName(request.getName()); agreement.setDescription(request.getDescription()); agreement.setStartDate(formatAgreementDate(request.getStartDate())); // Set plan Plan plan = new Plan(); plan.setId(request.getPlanId()); agreement.setPlan(plan); // Set payer Payer payer = new Payer(); payer.setPaymentMethod("paypal"); agreement.setPayer(payer); // Create agreement Agreement createdAgreement = agreement.create(apiContext); // Save subscription record saveSubscriptionRecord(createdAgreement, request); log.info("Created billing agreement: {}", createdAgreement.getId()); return createdAgreement; } catch (PayPalRESTException e) { log.error("Failed to create billing agreement", e); throw e; } } public Agreement executeBillingAgreement(String agreementId, String token) throws PayPalRESTException { try { Agreement agreement = new Agreement(); agreement.setToken(token); Agreement executedAgreement = agreement.execute(apiContext, agreementId); // Update subscription record updateSubscriptionRecord(executedAgreement); log.info("Executed billing agreement: {}", agreementId); return executedAgreement; } catch (PayPalRESTException e) { log.error("Failed to execute billing agreement: {}", agreementId, e); throw e; } } public Agreement getAgreement(String agreementId) throws PayPalRESTException { try { return Agreement.get(apiContext, agreementId); } catch (PayPalRESTException e) { log.error("Failed to get agreement: {}", agreementId, e); throw e; } } public void suspendAgreement(String agreementId) throws PayPalRESTException { try { Agreement agreement = new Agreement(); agreement.setId(agreementId); AgreementStateDescriptor stateDescriptor = new AgreementStateDescriptor(); stateDescriptor.setNote("Suspended by merchant"); agreement.suspend(apiContext, stateDescriptor); // Update subscription status updateSubscriptionStatus(agreementId, "SUSPENDED"); log.info("Suspended agreement: {}", agreementId); } catch (PayPalRESTException e) { log.error("Failed to suspend agreement: {}", agreementId, e); throw e; } } public void reactivateAgreement(String agreementId) throws PayPalRESTException { try { Agreement agreement = new Agreement(); agreement.setId(agreementId); AgreementStateDescriptor stateDescriptor = new AgreementStateDescriptor(); stateDescriptor.setNote("Reactivated by merchant"); agreement.reactivate(apiContext, stateDescriptor); // Update subscription status updateSubscriptionStatus(agreementId, "ACTIVE"); log.info("Reactivated agreement: {}", agreementId); } catch (PayPalRESTException e) { log.error("Failed to reactivate agreement: {}", agreementId, e); throw e; } } private Plan updatePlanStatus(String planId, String status) throws PayPalRESTException { List<Patch> patchRequest = new ArrayList<>(); Patch patch = new Patch(); patch.setOp("replace"); patch.setPath("/"); patch.setValue(Collections.singletonMap("state", status)); patchRequest.add(patch); Plan plan = new Plan(); plan.setId(planId); return plan.update(apiContext, patchRequest); } private String formatAgreementDate(Instant startDate) { // Format: 2020-01-01T00:00:00Z return DateTimeFormatter.ISO_INSTANT.format(startDate); } private void saveSubscriptionRecord(Agreement agreement, CreateAgreementRequest request) { SubscriptionRecord record = SubscriptionRecord.builder() .agreementId(agreement.getId()) .planId(request.getPlanId()) .state(agreement.getState()) .description(request.getDescription()) .startDate(request.getStartDate()) .createdAt(Instant.now()) .build(); subscriptionRepository.save(record); } private void updateSubscriptionRecord(Agreement agreement) { subscriptionRepository.findByAgreementId(agreement.getId()) .ifPresent(record -> { record.setState(agreement.getState()); record.setUpdatedAt(Instant.now()); subscriptionRepository.save(record); }); } private void updateSubscriptionStatus(String agreementId, String status) { subscriptionRepository.findByAgreementId(agreementId) .ifPresent(record -> { record.setState(status); record.setUpdatedAt(Instant.now()); subscriptionRepository.save(record); }); } }

REST Controllers

1. Payment Controller:

@RestController @RequestMapping("/api/payments") @Slf4j @Validated public class PaymentController { private final PayPalPaymentService paymentService; private final AppConfig appConfig; public PaymentController(PayPalPaymentService paymentService, AppConfig appConfig) { this.paymentService = paymentService; this.appConfig = appConfig; } @PostMapping("/create") public ResponseEntity<PaymentResponse> createPayment(@Valid @RequestBody CreatePaymentRequest request) { try { // Set redirect URLs if not provided if (request.getReturnUrl() == null) { request.setReturnUrl(appConfig.getFrontendUrl() + "/payment/success"); } if (request.getCancelUrl() == null) { request.setCancelUrl(appConfig.getFrontendUrl() + "/payment/cancel"); } Payment payment = paymentService.createPayment(request); PaymentResponse response = PaymentResponse.builder() .paymentId(payment.getId()) .state(payment.getState()) .approvalUrl(findApprovalUrl(payment)) .createdAt(Instant.now()) .build(); return ResponseEntity.ok(response); } catch (PayPalRESTException e) { log.error("Payment creation failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @PostMapping("/execute") public ResponseEntity<Payment> executePayment(@Valid @RequestBody ExecutePaymentRequest request) { try { Payment payment = paymentService.executePayment(request.getPaymentId(), request.getPayerId()); return ResponseEntity.ok(payment); } catch (PayPalRESTException e) { log.error("Payment execution failed: {}", request.getPaymentId(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @GetMapping("/{paymentId}") public ResponseEntity<Payment> getPayment(@PathVariable String paymentId) { try { Payment payment = paymentService.getPayment(paymentId); return ResponseEntity.ok(payment); } catch (PayPalRESTException e) { log.error("Failed to get payment: {}", paymentId, e); return ResponseEntity.notFound().build(); } } @PostMapping("/{paymentId}/refund") public ResponseEntity<Refund> refundPayment(@PathVariable String paymentId, @Valid @RequestBody RefundRequest request) { try { Refund refund = paymentService.refundPayment(paymentId, request); return ResponseEntity.ok(refund); } catch (PayPalRESTException e) { log.error("Refund failed for payment: {}", paymentId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } private String findApprovalUrl(Payment payment) { return payment.getLinks().stream() .filter(link -> "approval_url".equals(link.getRel())) .findFirst() .map(Links::getHref) .orElseThrow(() -> new RuntimeException("Approval URL not found")); } }

2. Subscription Controller:

@RestController @RequestMapping("/api/subscriptions") @Slf4j @Validated public class SubscriptionController { private final PayPalSubscriptionService subscriptionService; private final AppConfig appConfig; public SubscriptionController(PayPalSubscriptionService subscriptionService, AppConfig appConfig) { this.subscriptionService = subscriptionService; this.appConfig = appConfig; } @PostMapping("/plans") public ResponseEntity<Plan> createPlan(@Valid @RequestBody CreatePlanRequest request) { try { Plan plan = subscriptionService.createBillingPlan(request); return ResponseEntity.ok(plan); } catch (PayPalRESTException e) { log.error("Plan creation failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @PostMapping("/agreements") public ResponseEntity<Agreement> createAgreement(@Valid @RequestBody CreateAgreementRequest request) { try { Agreement agreement = subscriptionService.createBillingAgreement(request); return ResponseEntity.ok(agreement); } catch (PayPalRESTException e) { log.error("Agreement creation failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @PostMapping("/agreements/{agreementId}/execute") public ResponseEntity<Agreement> executeAgreement(@PathVariable String agreementId, @RequestParam String token) { try { Agreement agreement = subscriptionService.executeBillingAgreement(agreementId, token); return ResponseEntity.ok(agreement); } catch (PayPalRESTException e) { log.error("Agreement execution failed: {}", agreementId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @GetMapping("/agreements/{agreementId}") public ResponseEntity<Agreement> getAgreement(@PathVariable String agreementId) { try { Agreement agreement = subscriptionService.getAgreement(agreementId); return ResponseEntity.ok(agreement); } catch (PayPalRESTException e) { log.error("Failed to get agreement: {}", agreementId, e); return ResponseEntity.notFound().build(); } } @PostMapping("/agreements/{agreementId}/suspend") public ResponseEntity<Void> suspendAgreement(@PathVariable String agreementId) { try { subscriptionService.suspendAgreement(agreementId); return ResponseEntity.ok().build(); } catch (PayPalRESTException e) { log.error("Failed to suspend agreement: {}", agreementId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } @PostMapping("/agreements/{agreementId}/reactivate") public ResponseEntity<Void> reactivateAgreement(@PathVariable String agreementId) { try { subscriptionService.reactivateAgreement(agreementId); return ResponseEntity.ok().build(); } catch (PayPalRESTException e) { log.error("Failed to reactivate agreement: {}", agreementId, e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } }

Webhook Handling

1. Webhook Controller:

@RestController @RequestMapping("/webhooks/paypal") @Slf4j public class PayPalWebhookController { private final PayPalConfig payPalConfig; private final PaymentRepository paymentRepository; private final SubscriptionRepository subscriptionRepository; public PayPalWebhookController(PayPalConfig payPalConfig, PaymentRepository paymentRepository, SubscriptionRepository subscriptionRepository) { this.payPalConfig = payPalConfig; this.paymentRepository = paymentRepository; this.subscriptionRepository = subscriptionRepository; } @PostMapping public ResponseEntity<String> handleWebhook(@RequestBody String payload, @RequestHeader("PAYPAL-TRANSMISSION-ID") String transmissionId, @RequestHeader("PAYPAL-CERT-ID") String certId, @RequestHeader("PAYPAL-AUTH-ALGO") String authAlgo, @RequestHeader("PAYPAL-TRANSMISSION-SIG") String transmissionSig, @RequestHeader("PAYPAL-TRANSMISSION-TIME") String transmissionTime) { try { // Verify webhook signature if (!verifyWebhookSignature(payload, transmissionId, certId, authAlgo, transmissionSig, transmissionTime)) { log.warn("Invalid webhook signature"); return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); } // Parse webhook event JsonNode event = new ObjectMapper().readTree(payload); String eventType = event.get("event_type").asText(); JsonNode resource = event.get("resource"); log.info("Received PayPal webhook: {}", eventType); // Handle different event types switch (eventType) { case "PAYMENT.SALE.COMPLETED": handlePaymentCompleted(resource); break; case "PAYMENT.SALE.REFUNDED": handlePaymentRefunded(resource); break; case "BILLING.SUBSCRIPTION.ACTIVATED": handleSubscriptionActivated(resource); break; case "BILLING.SUBSCRIPTION.CANCELLED": handleSubscriptionCancelled(resource); break; case "BILLING.SUBSCRIPTION.EXPIRED": handleSubscriptionExpired(resource); break; default: log.debug("Unhandled webhook event: {}", eventType); } return ResponseEntity.ok("Webhook processed"); } catch (Exception e) { log.error("Webhook processing failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } private boolean verifyWebhookSignature(String payload, String transmissionId, String certId, String authAlgo, String transmissionSig, String transmissionTime) { try { // PayPal provides a Webhook signature verification utility // This is a simplified version - use PayPal's official verification in production Map<String, String> headers = new HashMap<>(); headers.put("PAYPAL-TRANSMISSION-ID", transmissionId); headers.put("PAYPAL-CERT-ID", certId); headers.put("PAYPAL-AUTH-ALGO", authAlgo); headers.put("PAYPAL-TRANSMISSION-SIG", transmissionSig); headers.put("PAYPAL-TRANSMISSION-TIME", transmissionTime); // In production, use: WebhookEvent.verifyEvent(headers, payload, webhookId, context) return true; // Simplified for example } catch (Exception e) { log.error("Webhook signature verification failed", e); return false; } } private void handlePaymentCompleted(JsonNode resource) { String saleId = resource.get("id").asText(); String paymentId = extractPaymentIdFromSale(resource); paymentRepository.findByPaymentId(paymentId).ifPresent(payment -> { payment.setState("COMPLETED"); payment.setUpdatedAt(Instant.now()); paymentRepository.save(payment); log.info("Payment completed: {}", paymentId); }); } private void handlePaymentRefunded(JsonNode resource) { String refundId = resource.get("id").asText(); String saleId = resource.get("sale_id").asText(); log.info("Payment refunded: {} for sale: {}", refundId, saleId); } private void handleSubscriptionActivated(JsonNode resource) { String agreementId = resource.get("id").asText(); subscriptionRepository.findByAgreementId(agreementId).ifPresent(subscription -> { subscription.setState("ACTIVE"); subscription.setUpdatedAt(Instant.now()); subscriptionRepository.save(subscription); log.info("Subscription activated: {}", agreementId); }); } private void handleSubscriptionCancelled(JsonNode resource) { String agreementId = resource.get("id").asText(); subscriptionRepository.findByAgreementId(agreementId).ifPresent(subscription -> { subscription.setState("CANCELLED"); subscription.setUpdatedAt(Instant.now()); subscriptionRepository.save(subscription); log.info("Subscription cancelled: {}", agreementId); }); } private void handleSubscriptionExpired(JsonNode resource) { String agreementId = resource.get("id").asText(); subscriptionRepository.findByAgreementId(agreementId).ifPresent(subscription -> { subscription.setState("EXPIRED"); subscription.setUpdatedAt(Instant.now()); subscriptionRepository.save(subscription); log.info("Subscription expired: {}", agreementId); }); } private String extractPaymentIdFromSale(JsonNode sale) { // Extract payment ID from sale resource // This depends on your implementation return sale.get("parent_payment").asText(); } }

Error Handling and Validation

1. Global Exception Handler:

@RestControllerAdvice @Slf4j public class PayPalExceptionHandler { @ExceptionHandler(PayPalRESTException.class) public ResponseEntity<ErrorResponse> handlePayPalException(PayPalRESTException e) { log.error("PayPal API error", e); ErrorResponse error = ErrorResponse.builder() .code("PAYPAL_API_ERROR") .message("Payment service temporarily unavailable") .details(e.getDetails()) .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(error); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException e) { List<String> errors = e.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse error = ErrorResponse.builder() .code("VALIDATION_ERROR") .message("Invalid request parameters") .details(errors) .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } } @Data @Builder class ErrorResponse { private String code; private String message; private Object details; private Instant timestamp; }

Best Practices

1. Security Configuration:

@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .ignoringRequestMatchers("/webhooks/paypal") // PayPal webhooks don't use CSRF ) .authorizeHttpRequests(authz -> authz .requestMatchers("/webhooks/paypal").permitAll() .requestMatchers("/api/payments/**", "/api/subscriptions/**").authenticated() .anyRequest().permitAll() ); return http.build(); } }

2. Health Check:

@Component public class PayPalHealthIndicator implements HealthIndicator { private final PayPalPaymentService paymentService; public PayPalHealthIndicator(PayPalPaymentService paymentService) { this.paymentService = paymentService; } @Override public Health health() { try { // Try to get a known payment (or create a test payment) to verify connectivity paymentService.getPayment("test-payment-id"); return Health.up().withDetail("paypal", "connected").build(); } catch (Exception e) { return Health.down() .withDetail("paypal", "disconnected") .withDetail("error", e.getMessage()) .build(); } } }

Conclusion

Integrating the PayPal SDK with Java applications provides a robust, secure payment processing solution that scales from simple one-time payments to complex subscription management. The combination of PayPal's global payment infrastructure with Java's enterprise capabilities creates a powerful foundation for e-commerce applications, SaaS platforms, and digital services.

By leveraging the PayPal Java SDK, developers can implement payment features quickly while ensuring PCI compliance, handling global currencies, and providing a trusted payment experience for users. The comprehensive webhook support enables real-time payment status updates, making it possible to build responsive, event-driven payment systems that integrate seamlessly with business workflows.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper