# 💳 Implementing Razorpay Webhooks in Spring Boot

### 🚀 Real Problems I Faced & How I Solved Them

> **Building a payment integration is easy in a tutorial.  
> Making it actually work is where the real engineering begins.** 😅

While working on my **ShopEase e-commerce project**, I reached a point where payment creation and verification were already working. The next challenge was handling **Razorpay Webhooks** and processing payment events asynchronously.

* * *

## 🏗️ My Payment Architecture

ShopEase uses a **microservices-based backend**, with a separate `payment-service` running on port `8085`.

### 🔄 Before Webhooks

```plaintext
🛒 Frontend
     ↓
🌐 API Gateway
     ↓
💳 Payment Service
     ↓
💰 Razorpay
```

### ⚡ After Adding Webhooks

```plaintext
💰 Razorpay
     ↓
🔔 Webhook
     ↓
💳 Payment Service
     ↓
🔐 Verify Signature
     ↓
🔎 Identify Event
     ↓
🗄️ Update Payment
     ↓
💾 Store Event ID
```

* * *

## 😵‍💫 Challenge #1 — Duplicate Webhooks

One of the first things I realized was that a webhook **may be delivered more than once**.

If I process the same event twice, I could accidentally execute the same business logic multiple times.

So I introduced a `webhook_events` table and stored the webhook's `eventId`.

### 🔐 Why?

```plaintext
Webhook received
      ↓
Already processed?
   ↙       ↘
 YES        NO
  ↓          ↓
Ignore     Process
             ↓
        Save Event ID
```

This gave my webhook processing **idempotency**.

* * *

## 🔑 Challenge #2 — Webhook Secret ≠ API Secret

This one was important.

I learned that the **Razorpay Webhook Secret is different from the Razorpay API Key Secret**.

Instead of hardcoding the secret, I kept it in an environment variable:

```plaintext
razorpay.webhook.secret=${RAZORPAY_WEBHOOK_SECRET}
```

🔒 **Never put actual secrets directly into your GitHub repository.**

* * *

## 🛡️ Challenge #3 — Verifying the Webhook Signature

I couldn't simply trust every request that reached my endpoint.

The request contains:

```plaintext
X-Razorpay-Signature
```

I used Razorpay's signature verification mechanism with the **raw request payload** and my webhook secret.

```plaintext
📨 Incoming Request
        ↓
🔐 Extract Signature
        ↓
🧮 Generate/Verify HMAC
        ↓
   Valid? ────── No → ❌ Reject
        ↓
       Yes
        ↓
     ✅ Process
```

* * *

## 🚫 Challenge #4 — The Mysterious `403 Forbidden`

This was one of those errors where I initially thought:

> "Controller mein hi kuch problem hai." 😅

But the controller wasn't the problem.

**Spring Security was blocking the request.**

My normal APIs require authentication, but Razorpay's webhook doesn't have my application's JWT.

So I specifically permitted:

```plaintext
.requestMatchers("/payments/webhook").permitAll()
```

and disabled CSRF for the API.

### 💡 Lesson

> A perfectly written controller is useless if security blocks the request before it reaches the controller.

* * *

## 🧪 Challenge #5 — Testing Without Razorpay

While testing locally through Postman, I initially didn't have the real Razorpay signature.

A fake signature such as:

```plaintext
test-signature
```

correctly resulted in:

```plaintext
401 Unauthorized
```

And honestly, that was **good news**. 😂

It meant my signature verification was actually doing its job.

So I created a **Postman Pre-request Script** to generate the HMAC-SHA256 signature automatically whenever I changed the webhook payload.

* * *

## 🫠 Challenge #6 — "Payment Record Not Found"

This was probably one of my most interesting debugging moments.

The signature was valid ✅  
  
The event was recognized ✅

But:

```plaintext
{
  "success": false,
  "message": "Payment record not found"
}
```

### 🔍 Root Cause

The Razorpay **Order ID didn't match** the Order ID stored in my database.

It was literally a tiny character difference. 😭

```plaintext
Postman:
order_TT9CFIQhKkyXwR

Database:
order_TT9CFlQkHkyXwR
```

For humans, they look almost identical.

For a database?

**Completely different values.** 💀

After correcting the Order ID, the webhook successfully found the payment.

* * *

## 🐳 Challenge #7 — XAMPP vs Docker MySQL

This one caused a lot of confusion.

My application was using:

```plaintext
localhost:3308
```

while I was checking MySQL through XAMPP/phpMyAdmin on:

```plaintext
localhost:3306
```

I eventually discovered that port `3308` was actually mapped to my **Docker MySQL container**:

```plaintext
Windows :3308
      ↓
🐳 Docker
      ↓
MySQL :3306
```

The container was:

```plaintext
shopease-mysql
mysql:8.4
0.0.0.0:3308 → 3306
```

So instead of trying to move XAMPP to port `3308`, I connected directly to the Docker MySQL container.

### 🧠 Biggest Lesson Here

> **Before changing a port, first find out which process is already using it.**

* * *

## ✅ Testing `payment.captured`

After fixing the database issue, I tested:

```plaintext
payment.captured
```

The webhook returned:

```plaintext
{
  "success": true,
  "message": "Webhook signature verified",
  "event": "payment.captured"
}
```

with:

```plaintext
HTTP 200 OK
```

More importantly, I didn't stop at the API response.

I checked the database and confirmed:

```plaintext
CREATED
   ↓
SUCCESS
```

That confirmed the **actual business logic worked**, not just the HTTP response.

* * *

## 🔁 Testing Idempotency

Then came the real test.

I sent the **exact same webhook again** using the same event ID.

Instead of processing it again, the application returned:

```plaintext
{
  "success": true,
  "message": "Webhook already processed",
  "event": "payment.captured"
}
```

🎯 **Duplicate processing successfully prevented!**

* * *

## ❌ Testing `payment.failed`

I also tested:

```plaintext
payment.failed
```

The webhook was successfully processed and the database payment status changed to:

```plaintext
FAILED
```

So both major payment events were working:

| Event | Result |
| --- | --- |
| 💰 `payment.captured` | ✅ SUCCESS |
| ❌ `payment.failed` | ✅ FAILED |
| 🔁 Duplicate event | ✅ Blocked |
| 🔐 Invalid signature | ✅ Rejected |

* * *

# 🏁 Final Architecture

```plaintext
                    💰 Razorpay
                         │
                         │ 🔔 Webhook
                         ↓
                POST /payments/webhook
                         │
                         ↓
                🛡️ Spring Security
                         │
                         ↓
                🔐 Signature Check
                         │
                         ↓
                 🔎 Event Detection
                    ↙           ↘
           payment.captured   payment.failed
                  ↓                 ↓
             Find Payment      Find Payment
                  ↓                 ↓
               SUCCESS           FAILED
                    ↘           ↙
                         ↓
                  🔁 Check Event ID
                         ↓
                  Already Processed?
                    ↙          ↘
                  YES           NO
                   ↓             ↓
                Ignore       💾 Save Event
                                 ↓
                              ✅ 200 OK
```

* * *

# 🧠 What This Actually Taught Me

The biggest lesson wasn't just **"how to implement Razorpay Webhooks."**

It was learning how to debug a **complete backend system**.

### 🔐 1. Security matters

A controller can't help if Spring Security blocks the request.

### 🎯 2. IDs must match exactly

A single character difference can break a database lookup.

### 🐳 3. Understand your infrastructure

Docker port mapping can completely change where your application is actually connecting.

### 🔁 4. Webhooks must be idempotent

The same event shouldn't execute your business logic multiple times.

### 🗄️ 5. Verify the database

`200 OK` doesn't necessarily mean your business logic worked.

### 🐛 6. Debugging is part of development

```plaintext
💻 Code
   ↓
🧪 Test
   ↓
❌ Error
   ↓
🔎 Find Root Cause
   ↓
🔧 Fix
   ↓
🧪 Test Again
   ↓
🗄️ Verify Database
   ↓
✅ Done
```

* * *

# 🚀 Current Status

My Razorpay Webhook implementation currently supports:

*     
    ✅ Webhook endpoint  
    
*     
    ✅ Signature verification  
    
*     
    ✅ `payment.captured`  
    
*     
    ✅ `payment.failed`  
    
*     
    ✅ Payment status updates  
    
*     
    ✅ Duplicate webhook detection  
    
*     
    ✅ Idempotent processing  
    
*     
    ✅ Database verification  
    

The next step is to extend the webhook flow beyond payment status updates and connect it with the **complete post-payment business logic**, such as order confirmation and cart clearing.

* * *

# 💡 Final Takeaway

Implementing Razorpay Webhooks taught me something important:

> **Real-world backend development isn't just about writing code. It's about understanding what happens when that code meets security, databases, infrastructure, external APIs, and unexpected errors.**

The most valuable part of this implementation wasn't simply making the webhook work.

It was debugging:

**403 errors → invalid signatures → incorrect Order IDs → Docker/MySQL conflicts → duplicate events → database verification.** 😎🔥

And that's what made this feature much more realistic than simply following an integration tutorial.

* * *

## 🛒 ShopEase — Payment Service

### 💳 Razorpay Webhook Integration 🚀

**Learn → Build → Break → Debug → Fix → Improve.** 💻🔥
