DEV Community

zeeshan khan
zeeshan khan

Posted on

Webhook-only 'paid': wiring Google sign-in and Razorpay for a one-time Rs 3,999 product

Content365 sells one thing: a year of branded social media posts for one business, for Rs 3,999 including GST, paid once. I built it, and the payment path is small enough to describe in one post. The design goal was boring on purpose: exactly one way for an order to become paid, and nothing the browser says can change that.

Here is the flow and the reasoning behind each piece. No keys and no secrets, just the shape.

Sign-in stores as little as possible. Customers sign in with Google. We keep four things: Google's stable account id, the email, the name and the profile picture. We do not keep Google access or refresh tokens, because we never call Google on the user's behalf. The session is an HttpOnly cookie that lasts 30 days. Fewer stored credentials means fewer things to leak and fewer things to rotate.

The order comes first, payment second. After sign-in, the customer creates a brand. The only required field is the business name; the logo and details can come later. Creating the brand creates an order, and that order starts on the free plan: every account gets 2 free posts for its brand every 48 hours. If the customer later pays, the same order continues to the full 365 days and keeps the free posts already made. A customer who clicks 'Buy now' gets an order marked as a purchase, which skips the free previews and goes straight to checkout.

The point of one order for both paths is that nothing has to be migrated at the moment of payment. Paying only changes the order's state.

The server sets the price. The browser never sends an amount. When checkout opens, the server creates the Razorpay order for 399900 paise. Money is stored as integer paise, which avoids floating-point drift, and the GST split is fixed in code: Rs 3,388.98 plus Rs 610.02 at 18%. If someone edits the page's JavaScript, the worst they can do is break their own checkout.

Only the signed webhook means 'paid'. Razorpay Checkout runs in the browser and calls a success handler when the customer completes payment. It is tempting to mark the order paid right there. We don't. The browser callback only updates the screen to say the payment is being confirmed. The order is marked paid when Razorpay's server calls our webhook endpoint with a signed payload and the signature checks out. Then, and only then, the order is queued for its full year.

Why be this strict? Because the browser is the one party in the transaction we cannot trust. It can be closed mid-flow, replayed or edited. The webhook comes from Razorpay, signed, server to server. A single source of truth also keeps support simple. If a customer says they paid and the order is not paid, the question is always 'did the webhook arrive?', never 'which of three code paths ran?'.

Two practical notes if you build this yourself. Webhooks can arrive more than once and out of order, so make the transition to paid idempotent. And keep checkout behind a server-side switch, as we do, so you can turn buying off without a deploy if something upstream misbehaves.

Analytics follow the same rule. Our Meta Conversions API events are built on the server, and the Meta pixel never loads on signed-in pages. The server reports a Purchase only for a real Razorpay payment reference. Demo orders and orders our team marks as paid by hand never count as purchases, and those server events carry only a hashed email and a user id. A test can't inflate the purchase numbers.

On refunds and changes, we keep it simple and honest while the written policy is finalised: they are agreed case by case before work begins, and we ask buyers to ask us before paying what can be changed.

What I would tell anyone building a one-time product in India: keep the price on the server, store paise rather than rupees, let exactly one signed message decide 'paid', and tie purchase events to real payment references. None of it is clever. All of it saves you a bad day.

If you want to see the flow from the customer side, https://content365.si lets you make 2 free posts every 48 hours with no payment, and https://content365.si/offer is the full year at Rs 3,999 including GST per brand, one time. We don't post for you, and Reels are not included yet. Questions about the wiring are welcome in the comments.

Top comments (0)