McTaba Labs logo
Bonaventure OgetoBy Bonaventure Ogeto|

Handle Paystack Webhooks in Spring Boot

To handle Paystack webhooks in Spring Boot, create a controller that reads the raw body with @RequestBody String, computes an HMAC SHA-512 hash using javax.crypto.Mac, compares it to the X-Paystack-Signature header, and processes the event asynchronously. Return ResponseEntity.ok().

Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(csrf -> csrf.ignoringRequestMatchers("/webhooks/**"))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/webhooks/**").permitAll()
                .anyRequest().authenticated()
            );
        return http.build();
    }
}

Webhook Controller

@RestController
@RequestMapping("/webhooks")
public class WebhookController {
    @Value("${paystack.secret-key}")
    private String secretKey;

    @Autowired
    private PaystackService paystackService;

    @PostMapping("/paystack")
    public ResponseEntity<String> handleWebhook(
            @RequestBody String body,
            @RequestHeader("X-Paystack-Signature") String signature) {

        if (!verifySignature(body, signature)) {
            return ResponseEntity.badRequest().body("Invalid signature");
        }

        // Parse and process
        ObjectMapper mapper = new ObjectMapper();
        try {
            JsonNode event = mapper.readTree(body);
            String eventType = event.get("event").asText();
            JsonNode data = event.get("data");

            processEvent(eventType, data);
        } catch (Exception e) {
            // Log error but still return 200
        }

        return ResponseEntity.ok("OK");
    }

    private boolean verifySignature(String body, String signature) {
        try {
            Mac mac = Mac.getInstance("HmacSHA512");
            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA512");
            mac.init(keySpec);
            byte[] hash = mac.doFinal(body.getBytes());

            StringBuilder hexString = new StringBuilder();
            for (byte b : hash) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }

            return MessageDigest.isEqual(
                hexString.toString().getBytes(),
                signature.getBytes()
            );
        } catch (Exception e) {
            return false;
        }
    }

    private void processEvent(String eventType, JsonNode data) {
        if ("charge.success".equals(eventType)) {
            String reference = data.get("reference").asText();
            paystackService.verifyAndFulfill(reference);
        }
    }
}

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Testing

@Test
public void testValidWebhook() throws Exception {
    String payload = "{\"event\":\"charge.success\",\"data\":{\"reference\":\"test\"}}";
    Mac mac = Mac.getInstance("HmacSHA512");
    SecretKeySpec keySpec = new SecretKeySpec("sk_test_xxx".getBytes(), "HmacSHA512");
    mac.init(keySpec);
    String signature = Hex.encodeHexString(mac.doFinal(payload.getBytes()));

    mockMvc.perform(post("/webhooks/paystack")
        .content(payload)
        .contentType(MediaType.APPLICATION_JSON)
        .header("X-Paystack-Signature", signature))
        .andExpect(status().isOk());
}

Ship Payments Faster

Key Takeaways

  • Use @RequestBody String to get the raw body for HMAC computation.
  • javax.crypto.Mac with HmacSHA512 handles the signature computation.
  • Disable CSRF for the webhook endpoint in your SecurityFilterChain.
  • Use @Async for non-blocking event processing after returning 200.
  • Make handlers idempotent. Paystack retries failed deliveries.
  • Spring Security configuration must permit the webhook URL without authentication.

Frequently Asked Questions

Why does my webhook return 403?
Your Spring Security configuration is probably blocking the webhook URL or CSRF is enabled. Add the webhook path to CSRF ignore list and permit it without authentication.
Should I use @Async for webhook processing?
Yes. @Async lets you return 200 immediately and process in a separate thread. Make sure to enable @EnableAsync on your configuration class.
Can I use Spring Events for webhook processing?
Yes. Publish an ApplicationEvent from the controller and handle it with an @EventListener. This decouples the webhook receipt from processing.

Learn Paystack Integration

KES 2,999

The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.

Start Paystack Course

Not ready? Create Free Account