What changes when the money never touches your balance sheet
Most platforms that sell on your behalf sit in the middle of the payment. We decided not to. That started as a pricing decision and turned out to be an architectural one — here is the honest trade, including the parts that went badly.

Disclosure
I build Prolaud, the platform described below. Everything here is from our own implementation, including the parts that went badly. There are no competitor prices quoted in this article, because I would only be guessing at them.
Most platforms that sell things on behalf of other people sit in the middle of the payment. A student pays, the money lands with the platform, the platform keeps a percentage, and some time later the rest reaches the person who actually taught the class.
We decided not to do that. Payments settle into the creator's own gateway account, not ours. We take 0% of every sale and charge a subscription instead.
That started as a pricing decision. It turned out to be an architectural one, and the trade-off is more interesting than the marketing line suggests. If you are a teacher choosing a platform, the second half of this article tells you what the choice will actually feel like. If you are an engineer deciding whether to sit in the payment path, the whole thing is for you.
What staying out of the payment path removes
A whole class of reconciliation
If funds never arrive in our account, there is nothing to hold, nothing to split, and no ledger of who is owed what. We do not run payouts, so we do not have payout failures, payout schedules, minimum thresholds, or the support queue that comes with all three.
The hardest bug in a marketplace is usually money that exists in one system and not another — a sale the platform recorded but the bank never saw, or a settlement the bank made that the platform cannot attribute. Reconciliation code is where small teams go to die, because every edge case involves someone's actual money and none of them can be resolved by reading the code. We deleted the category rather than solving it.
Float, and the temptation that comes with it
Platforms holding other people's money end up with a balance that looks like working capital. It is not — it is a liability wearing a friendly number — but it sits in the same account and it is spendable. That is a governance problem before it is a technical one, and the simplest way to not have it is to never receive the money.
This matters more in a small company than a large one. A big platform has a finance function whose job is to keep that line bright. A three-person team does not, and the first time cash is tight the temptation is real. Architecture that makes a bad decision impossible is worth more than a policy that makes it forbidden.
Regulatory surface
Handling funds on behalf of others is a regulated activity in most places, and the rules differ by market. In India that lands you in payment-aggregator territory, with its own licensing expectations and its own timelines. Staying out of the payment path means the gateway's compliance is the gateway's problem: Razorpay is already an authorised aggregator, the creator's account is theirs, and we are a piece of software that reads notifications.
For a small team selling into India first, that difference is measured in months of work you never have to do.
What it costs
This is the part nobody puts on a pricing page. The 0% is real, and so is the bill for it — just paid in a different currency.
Every creator has to onboard their own gateway
We support INR through Razorpay and USD through Stripe, and either way the creator brings their own keys. That means our activation funnel now contains someone else's KYC process, which we neither control nor can debug. A teacher who has finished building a course still cannot sell it until a third party approves their documents — PAN, bank proof, sometimes a business registration, sometimes a request for a document they did not expect.
When that stalls, the teacher is not annoyed with Razorpay. They are annoyed with us, because we are the screen they are looking at. That is a fair reaction and there is nothing to argue about in it.
We handle it by letting them publish anyway and gating only checkout. The site goes live, the storefront renders, the course is visible, the share links work — and purchases stay paused behind an explicit message rather than a silent failure at the payment step. In the code that refusal is spelled out in the order record itself: purchase blocked to avoid collecting un-routable funds. It is a worse experience than instant selling and a much better one than taking money we cannot route to anybody.
Failure modes multiply by tenant
With one platform account, a gateway problem is one incident with one cause and one fix. With bring-your-own keys, it is N incidents with N different causes: expired keys, a webhook secret that was rotated and not updated, an account put under review, a creator who revoked access while debugging something else, a test-mode key pasted into a live field.
Each one looks identical from the student's side — a payment that does not complete — and completely different from ours.
The practical answer is that support tooling stops being a nice-to-have and becomes part of the product. A school needs to be able to see, without asking us, whether its gateway is connected, whether its webhook exists, and whether the last delivery succeeded. Anything less and every gateway hiccup becomes a support ticket that only we can close, which is the exact operational load we were trying to avoid.
Webhooks become the only source of truth
Since we never see the money, we only learn about a sale when the gateway tells us. There is no balance to inspect, no settlement file to reconcile against, no second opinion. The webhook is not a convenience — it is the entire channel.
We subscribe to eleven events across two rails. The list is worth showing in full, because every entry earns its place and the interesting ones are not the obvious ones:
| Event | What it means | What we do |
|---|---|---|
| payment.captured | Money actually taken | Mark the order paid, create the enrolment |
| payment.failed | Attempt did not complete | Mark the order failed so it can be retried cleanly |
| order.paid | Order fully settled | Reconciliation backstop |
| refund.created / refund.processed / refund.failed | Money going back | Reconcile the sale (see the section on what we got wrong) |
| subscription.charged | Recurring instalment taken | Extend access for the period |
| subscription.pending | Charge attempted, not yet settled | Hold access, do not revoke |
| subscription.halted | Retries exhausted after failures | Not the same as cancelled — see below |
| subscription.cancelled / completed | Plan ended | Stop future access at the period end |
subscription.halted is the one that separates a careful implementation from a careless one. A halted subscription is not a cancelled subscription. Halted means the card kept failing and the gateway gave up retrying; cancelled means a human ended the plan. Treating them the same will either lock out a student whose bank declined a routine auto-debit, or keep serving someone who left three months ago. Both are bad, and only one of them generates an angry message — which is precisely why the other one goes unnoticed for so long.
The two properties that matter more than anything else
Idempotency, enforced where the race actually is
Webhooks arrive more than once. They arrive out of order. They arrive three hours late because the gateway was retrying while your server was restarting. A retry must not create a second enrolment, and it must not increment a coupon's usage count a second time either.
The common advice is to key every handler on the gateway's own event id. That works, and it is not what we do. We key on the order's own state machine instead: fulfilment re-reads the order inside the transaction, with a row lock, and returns immediately if it is already paid. The transition from pending to paid happens exactly once because the database says so, not because a deduplication table said so.
The reason to prefer this is that the race is not really between two webhook deliveries. It is between a webhook delivery and every other path that can also mark an order paid — a zero-price enrolment, an invite claim, a manual repair. A lock on the order covers all of them. A dedupe key on the gateway event id covers only the first.
Underneath that we still check for an existing enrolment on the same order id before creating one, which is belt and braces, and it costs one indexed lookup. Keep it. The cost of the check is nothing; the cost of a duplicate enrolment is a student who sees the same course twice and a revenue number that is quietly wrong.
Never trust the client
The browser saying a payment succeeded means nothing. It is a value on a page, and the page belongs to the person reading it. If access is granted on that signal you have built a paywall that anyone can walk through with the network tab open, and you will not find out, because nothing about it looks like an error.
Access changes only when a signature-verified webhook says so. Verification is an HMAC over the raw request body — not the parsed JSON, not a re-serialised object. If any middleware has already parsed and re-encoded the body by the time your handler sees it, key ordering or whitespace will differ and every signature you compute will be wrong. This is the single most common way a webhook integration fails on day one, and the symptom is a handler that rejects deliveries that are genuinely valid.
It also means the moment after checkout has to be designed as "we are confirming this" rather than "you are in." That is a product decision as much as a security one, and it is worth spending real design effort on, because it is the most anxious ten seconds a buyer will spend with you.
The incident that taught us the most
We changed our domain. The application moved, the redirects went in, everything kept working — and course payments silently stopped enrolling students. Payments were captured. Money reached the teachers. Nobody was enrolled.
Two things had gone wrong at once, and the combination is worth knowing about:
- Webhooks registered at the old host were still there, pointing at a URL that now answered with a redirect. Razorpay does not follow redirects on webhook delivery — reasonably, since blindly following a redirect with a signed payload is a bad idea. A 301 is a failed delivery, not a forwarded one.
- Razorpay's API cannot delete a webhook. You can create one and you can update one, but the stale entry cannot be removed programmatically. So "just clean it up on deploy" was not available to us.
The fix was a reconciliation routine that treats the account's webhook list as state to be converged rather than something to be created once: keep exactly one hook at the canonical URL, update it in place if it exists, and disable any others we recognise as ours. Plus a button in the dashboard that runs the same routine on demand, because the first person to notice a broken webhook is the school, not us.
The lesson, generalised
A webhook URL is not configuration you set at integration time. It is state held in somebody else's system, it drifts, and you cannot always delete the drift. Write the converge routine on day one and give the user a way to run it.
The deeper lesson is about detection. Every individual component behaved correctly. The gateway captured and reported. Our handler verified signatures properly. The redirect was a correct redirect. Nothing logged an error, because nothing had an error — the delivery simply never arrived. When the webhook is your only source of truth, the absence of a webhook is silent by construction, and you have to go looking for it on purpose: an alert on captured-but-not-fulfilled, not an alert on exceptions.
The part we got wrong, which is still partly wrong
Our webhook has two rails behind one URL. A delivery that matches a course order is fulfilled by the course handler; everything else — store products, payment links, plans, refunds — is passed to the storefront handler, which verifies the signature itself and reconciles accordingly.
That split is clean for sales and leaky for refunds. The storefront rail reconciles refunds properly. The course rail does not, because refund events are routed away from it by design — they carry no course order id to match on. So refunding a course payment returns the money and leaves the enrolment standing.
It is not a security hole; a refund is initiated by the school, not the student, so nobody can help themselves to free access. But it is exactly the bug the draft of this article warned about in the abstract before I checked our own code and found we had it. That is a genuinely useful demonstration of the point: refund.processed has to take access away again, and the reason people forget is that the sale path is the one you test. Every integration gets the happy path right. Refunds are written later, by someone in a hurry, and then never exercised.
If you are building this, write the revocation path at the same time as the grant path, in the same sitting, and make a real refund in test mode before you ship.
What this means if you are the teacher, not the engineer
All of the above has consequences you can feel, and they are worth being straight about.
- Settlement is between you and your gateway. Razorpay's standard settlement cycle applies to your account, on your terms, to your bank. No platform sits between the sale and the money, so there is no platform payout schedule to wait on and no minimum threshold to reach before you can be paid.
- You will do KYC once, with the gateway, before you can sell. This is the real cost of the model and it is front-loaded. Build the course while it is in progress rather than waiting — the storefront works, only checkout is paused.
- UPI, cards and netbanking are whatever your gateway offers, because it is your gateway. We do not gate payment methods, and we could not if we wanted to.
- Refunds are issued from your account. You control the decision and the timing.
- GST invoicing runs on your GSTIN. A platform that does not touch the money also does not become a party to the supply. Where a school has GST configured, the document issued is a tax invoice on its own number series; where it does not, it is a plain receipt, because issuing something that looks like a tax invoice without a GSTIN would be a fake tax document.
- The bill is a subscription, due whether you sold anything or not. That is the honest other side, and the next section is about it.
The part I would argue about
The honest counter-argument, and I hear it more than I expected, is that creators often *prefer* a percentage.
A cut only hurts when things are working. A subscription is due whether you sold anything this month or not, and for a teacher with an irregular calendar — exam season, then two quiet months — that is a real difference in felt risk even when the annual total is lower. Paying ₹0 in a month you earned ₹0 is worth something psychologically that a spreadsheet does not capture.
I do not think that is wrong. It is a genuine preference about *when* you want to feel the cost, and our model optimises for the person having a good month at the expense of the person having a quiet one.
And building the architecture around 0% means we cannot easily offer the other shape later. Taking a percentage would mean getting back into the payment path, which would mean holding funds, which would mean the reconciliation, the float and the regulatory surface we deliberately do not have. The decision is much closer to irreversible than a pricing page implies.
That is the actual trade: we bought a simpler system and less regulatory exposure, and we paid for it with a harder activation funnel and a pricing model we cannot reverse.
If you are deciding this for your own product
Sit in the payment path if your product's value is the marketplace itself, if buyers need one checkout across many sellers, if you need escrow because the two sides do not trust each other yet, or if the sellers are individuals who will never complete a KYC process on their own.
Stay out of it if the seller already has a business, if they would rather keep the full price than get discovery from you, and if you would rather not staff a payouts function.
Just go in knowing the activation cost is real, that your support surface grows with every tenant rather than every feature, and that "0% commission" is a sentence about your architecture before it is a sentence about your pricing.
See the model from the other side
Connect your own Razorpay or Stripe account, keep 100% of every sale, and pay a flat subscription instead of a cut. The free plan is genuinely free.
See pricing
Renu Rawat
Founder of prolaud.com. Helping teachers and creators build profitable, independent learning businesses without losing a cut of every sale to platform fees.
About the founderFrequently asked questions
- Does a zero-commission platform still cost me nothing per sale?
- No — you still pay your payment gateway its processing fee, because that is the cost of accepting money online anywhere. What you do not pay is a platform cut on top of it. Zero commission means the platform takes 0% of your sale price, not that the sale is free to process.
- Whose Razorpay account is the money going into?
- Yours. You connect your own Razorpay account for INR or Stripe for USD, and the gateway settles directly to your bank on its normal cycle. The platform is never in the payment path, which is why there is no platform payout schedule, no minimum payout threshold and no platform-held balance.
- What happens if I have not finished my gateway KYC yet?
- You can build and publish everything — the academy site, the course pages, the storefront — and only checkout stays paused, with a clear message rather than a silent failure. The alternative would be collecting money with nowhere to route it, which is worse for everyone.
- How does the platform know a student paid if it never sees the money?
- Through signature-verified webhooks from the gateway. When a payment is captured, the gateway notifies the platform, the signature is verified against a per-school secret, and only then is the order marked paid and the enrolment created. Nothing the browser reports can grant access.
- What stops a duplicate enrolment if the same webhook arrives twice?
- The order is re-read inside a database transaction with a row lock before anything changes, and the pending-to-paid transition happens exactly once. A redelivery three hours later finds the order already paid and does nothing. There is also a check for an existing enrolment on the same order as a second line of defence.
- Why can a platform not just delete its old webhooks when it changes domain?
- Razorpay's API supports creating and updating webhooks but not deleting them. Stale entries pointing at an old host have to be disabled and updated in place instead, and since webhook delivery does not follow redirects, a hook still pointing at a retired domain fails silently rather than forwarding.
- Is a subscription really cheaper than a commission?
- It depends entirely on your volume. Above a break-even point a flat fee is cheaper and the gap widens as you grow; below it, a percentage costs less and costs nothing at all in a month with no sales. The honest answer is to work out your own break-even rather than trusting either marketing claim.
Keep reading

Zero-Commission Course Platforms: Keep 100% of Your Course Revenue
Commission is the quiet tax on every sale you make. Here's what it really costs over a year, how to spot a true zero-commission platform, and how to keep what you earn.
Read
How to Accept Payments for Online Courses in India (UPI + Razorpay)
UPI, Razorpay, GST, installments, failed payments and refunds — everything an Indian educator needs to collect course fees cleanly and get paid straight to the bank.
Read
Razorpay charges for course creators — what you actually pay
The gateway fee is the number everyone asks about — and usually the smaller one. Here's how Razorpay's charges actually work for course sellers, and the cost most creators miss.
Read
UPI Payments for Online Courses: A Guide for Indian Educators
Native UPI checkout + your own Razorpay account + UPI as the first option = paid to your bank in about T+2, with 0% taken by the platform. The full setup, in order.
Read
GST for Online Course Creators in India: A Plain-English Guide
GST is the part course creators most want to ignore and most regret ignoring. Here's a plain-English overview — registration, rates, invoices and international sales — so you can stay clean.
Read