Fompey: The most elegant way to collect UPI payments

In May 2026, GradRight went through a company-wide restructuring, and my role went with it. Like most laid-off engineers, I did the obvious thing first — updated the resume, opened LinkedIn, started applying. But somewhere between the fortieth cold email and the fifteenth "we've decided to move forward with other candidates," I started asking a different question: what if I spent some of these hours building instead of only applying?
That question turned into Formpey — and I want to tell you what it is, why it exists, and what it took to get it production-ready.
Here's a workflow that plays out thousands of times a week across India: someone is organising a paid event — a college fest, a workshop, a coaching batch — and they need to (a) collect registrations and (b) collect money. So they do this:
Build a form in Google Forms.
Create a separate Razorpay payment link and paste it into the confirmation screen.
Export responses to a spreadsheet.
Open the Razorpay dashboard and manually match payments to responses by name and amount.
Step 4 is where things fall apart. Someone who paid gets marked unpaid and is wrongly chased. Someone whose card declined and who then paid by UPI shows up as two Razorpay records against one ambiguous spreadsheet row. It's manual, it's error-prone in exactly the way that costs real money, and it gets worse as attendance grows.
Existing tools don't close this gap. Google Forms has no payment concept at all. Razorpay Payment Pages take money well but aren't a form builder — no conditional logic, no file uploads, no responses table. Tally and Jotform are excellent, but they're built for a Western SMB: UPI is a second-class citizen, pricing is in dollars, and money typically routes through the vendor rather than staying with the merchant.
So I built the thing that was missing: a form builder where payment is a block, not an afterthought.
Formpey is a Notion-style form builder. You write your form the way you'd write a Notion doc — type content directly, hit / to insert a field. One of those blocks is a Payment block. Drop it in, publish, share a link or a QR code, and respondents pay by UPI without ever leaving the flow. The organizer opens one table and sees exactly who submitted and who paid — no spreadsheet, no manual matching.
The commercial shape is deliberately simple: merchants connect their own Razorpay account. Money settles directly to them. Formpey is never in the flow of funds, which means no payment-aggregator license, no settlement liability on my end, and 0% commission as a literal fact rather than a marketing line.
Anyone can build a form. The hard problem — the one that made this an actual engineering project instead of a weekend hack — is making sure the payment status shown to the organizer is never incorrect. Everything else in the product is downstream of that one requirement.
A few things that came out of taking that seriously:
The webhook is the sole source of truth. The client-side "payment successful" callback is optimistic UI only — I've observed it fire in production while the actual settlement was still pending. Trusting it would mean sometimes lying to the organizer.
Status transitions live in a table in code, not a WHERE clause. This one bit me for real. My original rule updated status only WHERE payment_status = 'pending'. That silently refused to settle a payment that had already been marked failed — but Razorpay allows retries inside the same order, so a respondent whose card declined and who then paid via UPI is the normal path, not an edge case. Their money got taken and nothing recorded it. Fixing this meant writing an explicit transition table (failed → paid: allowed, paid → anything: never) instead of leaning on a filter condition. It's now the bug story I tell anyone who asks what "production-ready" actually means.
Idempotency comes from the database, not application logic. A check-then-insert has a race window that two concurrent webhook deliveries will eventually find. A uniqueness constraint on the provider's event ID doesn't.
Amount is never computed client-side. The client sends answers, never a price. The server derives the amount from the frozen, published form schema — because a hidden field must never be able to price anything, and the client has no way to lie about a number it never had.
I load-tested the submission path at 8,762 requests with zero failures, sustained 287 req/s, and p99 latency of 207ms. I made a real ₹1 payment on my own phone and hand-reconciled it against the Razorpay dashboard — zero mismatches. I seeded 18 hostile CSV rows (formula injection attempts, emoji, Devanagari, embedded newlines) and confirmed every one opened safely in a spreadsheet. None of this was required to "have a form builder." All of it was required to have one I'd trust with someone else's money.
Formpey is built and production-verified, pre-launch. The public form scores 99 on mobile / 100 on desktop on PageSpeed — because the respondent filling this out is often on a mid-range Android over patchy 4G data, and every kilobyte of JavaScript is a kilobyte they're paying for. Pricing is simple: free forever with unlimited forms and responses at 0% commission, and a Pro tier at ₹199/month (or ₹1,999/year) that removes the Formpey badge and lifts a few limits.
I'm not posting this because it's finished — I'm posting it because building this, alone, over a couple of weeks after a layoff, taught me more about engineering discipline than most of my actual jobs did. Turns out the difference between "a form with a payment button" and "a payment system that happens to have a form in front of it" is most of the work.
If you organise events, run a coaching batch, or collect payments through forms today and want to be one of the first people to try it, I'd genuinely love the feedback. And if you're a fellow builder — I'll be sharing more of the specific engineering decisions (and the mistakes) as I get closer to public launch.
— Pankaj Kumar
0
1
0