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