Global Payment Gateway Troubles

Beyond the Integration: Why I Built a Custom Subscription Engine for Vocalis

Feb 23, 2026

Global Payment Gateway Troubles

Beyond the Integration: Why I Built a Custom Subscription Engine for Vocalis

Feb 23, 2026

Global Payment Gateway Troubles

Beyond the Integration: Why I Built a Custom Subscription Engine for Vocalis

Feb 23, 2026

Introduction — The Weekend Lie

If you’ve ever searched “How to add payments to your SaaS,” you’ll find dozens of cheerful tutorials.

“Just integrate Stripe.”
“Hook up a webhook.”
“Done in a weekend.”

That advice works — if you’re in the US.

If you’re an Indian founder trying to sell globally in USD, while building on Supabase, hosting on Render, and targeting creators across time zones — it’s not a weekend project.

It’s a systems problem.

And systems don’t forgive assumptions.

When I started building Vocalis, I assumed billing would be the easy part. I had already designed the content engine. The persona layer worked. The template renderer was stable. The AI outputs were structured.

Payments? That’s just plumbing.

I was wrong.

After being rejected by Lemon Squeezy.
After being rejected by Paddle.
After realizing Stripe Atlas wasn’t practical for me right now.

I had to build a production-ready billing system using Razorpay — one that could:

  • Handle international USD payments

  • Avoid currency misreporting

  • Support trial and non-trial flows

  • Allow launch offers without breaking renewals

  • Prevent duplicate subscriptions

  • Survive webhook chaos

This is the architecture I ended up building.

And why I’m glad I didn’t rush it.

The First Shock: The Metadata Mismatch

The first time I tested a USD payment, I was excited.

I ran a $19 Creator plan test.

Payment successful.

Webhook fired.

Database updated.

Everything looked clean.

Until I checked the logs.

Razorpay, in certain events, reported my home currency (INR) in the webhook payload — even though the plan was configured in USD.

Let that sink in.

If my backend trusted that metadata blindly:

A $19 subscription
Would appear as ₹19

That’s not a rounding error.

That’s a financial reporting disaster.

Now imagine:

  • Sending invoices with wrong currency symbols

  • Calculating revenue incorrectly

  • Emailing users the wrong plan amount

  • Reconciling payouts manually

This wasn’t cosmetic. It was structural.

The Fix: Amount-Based Currency Inference

I stopped trusting currency metadata.

Instead, I asked:

What’s invariant?

The unit amount.

My pricing structure was simple:

  • USD plans: under $100

  • INR plans: above ₹1000

That gap gave me leverage.

Inside my webhook handler, I built a logic gate:

if amount < 10000:  # Razorpay reports in smallest unit (cents/paise)    inferred_currency = "USD"else:    inferred_currency = "INR"

Now instead of trusting metadata, I derive currency from economic context.

It’s not pretty.

But it’s robust.

Invoices show the right symbol.
Emails show the right symbol.
Reports show the right currency.

The system adapts — not assumes.

That was the first architectural pivot.

The Bigger Problem: Plan Keys Are a Trap

Originally, I linked subscriptions using plan keys:

  • creator

  • agency

  • starter

Simple.

Readable.

Clean.

Until I introduced a Launch Offer.

Let’s say:

Creator Plan → $29/month
Launch Creator Plan → $19/month

Now what?

If both use the key creator, you can’t distinguish pricing lineage.

If you overwrite the plan, existing renewals break.

If you delete the launch plan, grandfathered customers lose mapping.

I realized something important:

Plan keys are human-friendly.
They are not database-friendly.

Architectural Pivot: UUID-Based Plan Versioning

I rebuilt the schema.

Instead of linking subscriptions to a plan_key, I linked them to a plan_id (UUID).

The subscription_plans table now looks like:



id (UUID)

key

price

currency

is_active

Key difference:

The frontend only shows plans where:

WHERE is_active = true

But the database keeps all historical plans.

To enforce safety, I added a Partial Unique Index:

CREATE UNIQUE INDEX unique_active_plan_keyON subscription_plans (key)WHERE is_active = true;

This ensures:

  • Only one active Creator plan at a time

  • Old plans remain in DB forever

  • Existing subscriptions remain valid

  • Grandfathered users never break

Now I can:

  • Launch discounted plans

  • Retire them later

  • Keep historical pricing intact

  • Scale without fear

This was not a tutorial solution.

This was a founder-who-got-burned solution.

The “None” Date Crash

Billing systems have a personality.

They behave differently based on configuration.

Initially, I used trial-based subscriptions.

Razorpay sent a start_at timestamp.

My DB parsed it.

Everything fine.

Then for launch, I switched to Immediate Charge (no trial).

Suddenly:

start_at = null

And PostgreSQL doesn’t politely accept "None".

It throws:

invalid input syntax for type date: "None"

On launch day, that would mean:

  • Payment succeeds

  • Webhook fires

  • DB crashes

  • User stuck in limbo

That’s the kind of bug that kills trust permanently.

The Surgical Date Fallback

Instead of assuming the timestamp would exist, I built defensive logic.

Inside webhook processing:

if start_at:    current_period_end = parse_timestamp(start_at)else:    current_period_end = today + timedelta(days=30)

Then, in Supabase upsert logic, I used conditional filters.

No nulls propagate.

No invalid date inserts.

If metadata is missing, the system derives.

That’s a pattern you’ll notice:

Every time an assumption broke, I replaced it with inference + fallback.

Because production systems are not polite.

Idempotency: The Composite Shield

Webhooks are unreliable.

They retry.
They duplicate.
They arrive out of order.

If you naively insert rows on every webhook call, you end up with:

  • 5 active subscriptions for one user

  • Broken dashboards

  • Corrupt analytics

So I designed what I call the Composite Shield.

Inside PostgreSQL:

UNIQUE (provider, provider_subscription_id, plan_key, status, current_period_end)

This ensures the same state cannot exist twice.

Then in Python, I implemented a strict state machine:

  1. Try update existing row

  2. If no row updated → insert

  3. If constraint violation → ignore duplicate

The database becomes the final arbiter of truth.

Not the webhook.
Not Razorpay.
Not my API.

The database.

This small mindset shift changes everything.

Why I Didn’t “Just Launch”

It would have been easy to:

  • Ignore currency mismatch

  • Ignore plan versioning

  • Ignore idempotency

  • Ignore null dates

  • Launch fast

  • Fix later

But billing bugs don’t just break features.

They break trust.

And trust is the only currency that matters for a solo founder.

I’ve rushed launches before.

I’ve shipped brittle foundations.

I’ve watched systems collapse under edge cases.

This time I didn’t.

The Emotional Layer

Building Vocalis is not my first attempt at building something meaningful.

I’ve built startups since 2005.
Most didn’t work.
Some survived.
None scaled globally.

Each time, I learned something.

This time I applied those lessons before launch.

Because the difference between a side project and a SaaS company is not UI polish.

It’s foundation integrity.

What This Architecture Now Enables

Because of these pivots, Vocalis now supports:

  • USD and INR seamlessly

  • Launch offers without renewal crashes

  • Grandfathered pricing models

  • Trial and immediate charge modes

  • Idempotent webhook handling

  • Reliable state transitions

That’s not glamorous.

But it’s scalable.

And scalable systems attract serious users — and serious investors.

Conclusion: Foundation Before Fame

The internet glorifies speed.

“Ship fast.”
“Launch messy.”
“Figure it out later.”

Sometimes that works.

But payments are not the place for chaos.

By facing these hurdles early, I’ve built a billing engine that:

  • Respects user timelines

  • Handles multi-currency cleanly

  • Avoids metadata traps

  • Survives webhook retries

  • Scales without schema fear

Vocalis launches tomorrow on Product Hunt.

But more importantly — Vocalis now rests on a foundation that won’t collapse under growth.

And for a solo founder rebuilding again —

That matters more than any upvote.

Experience it yourself:
👉 https://vocalis.so/launch