Dodo Payments vs Stripe: what changes when the provider is the seller
Notes from running both in production: what a merchant of record takes off a solo founder, where Dodo Payments and Stripe differ in code, and the bugs that showed no error.
Marcin Fratczak · updated Nov 15, 2026
I sell software to people in many countries, alone, from Poland. Every country has its own rules for VAT or sales tax on digital products. I do not want to register for tax around the world.
A merchant of record solves that. The provider is the seller on paper: it charges the customer, handles the tax and pays me the rest. Dodo Payments works this way, and Stripe has a managed option that does too.
I have both wired into my products, behind one switch. This page is what I learned doing it.
Four rules I do not break any more
Each of these comes from a real production bug.
1. One place changes the balance. Not the webhook, not the form action. One module does the accounting, and every provider's webhook is translated into the same shape before it gets there. Two copies of money arithmetic will drift apart at the first fix.
2. Accounting is idempotent and locks the row. The provider will send the same event twice. A record that is already paid must end the handler without changes. And two payments arriving at once will compute the balance from the same read unless the transaction locks the row.
3. The redirect from checkout is not proof of payment. Only a signed webhook is. The return page may say "thanks" and nothing more.
4. Who the seller is belongs in the terms, not in a constant. The provider's name is never hard-coded in legal text or on the pay button. It comes from configuration at render time. Otherwise switching providers leaves a sentence on the site saying people pay somebody else.
Where the two differ in code
| Stripe | Dodo Payments | |
|---|---|---|
| Price created on the fly | yes, price_data |
no: a "pay what you want" product and the amount on the cart line |
| Tax category | broad list | short closed list (digital_products, saas, e_book, edtech) |
| Price shown includes tax | tax_behavior: "inclusive" |
tax_inclusive: true |
| Short checkout form | configurable | minimal_address: true, the country is enough |
The first row decides the shape of the whole integration, not one line of it.
The third row matters more than it looks. With a merchant of record the buyer should pay the amount they saw. If you miss the flag, "$10" becomes $12.30 at checkout.
Bugs that showed no error
A webhook saved with a local address. My setup script read the site URL from the local config and registered the webhook at http://localhost:3301. The payment went through, the money was taken, and the record stayed "pending", because the call had nowhere to go. The script now takes the address from its own variable and stops when it sees localhost.
Stripe's managed payments and custom text. custom_text is not allowed there, and asking for consent to the terms needs the terms URL set in the account's public settings. Either one fails the session with a 500 and leaves nothing in the dashboard.
Customer ids do not travel. I keep a separate field per provider. The customer portal has to be opened at whoever took the money.
A column name outlives the provider. If a column is called stripeSessionId and holds a Dodo session, somebody will draw the wrong conclusion in three months. In Prisma you can rename the field and keep the column, so the migration is empty.
Switching between them
One function reads a variable and returns the provider. The default is the one that is safer for the business, not the one I integrated first.
A merchant of record can close an account for product categories it does not want. If a product is anywhere near that line, that provider should turn on only when I ask for it, not because a variable is missing.
How I test before selling
Test mode covers the whole path: checkout, 3-D Secure, webhook, record, notification. I run it on the production address, because only there the webhook has somewhere to arrive.
Then one real payment and one real refund. Only that checks the live signing secret, the payout and the refund path.
And I clean up. Test records can sit in a production table and look like real purchases.
Which one I would pick
If you are one person selling to the world and you do not want to think about tax, start with a merchant of record. That is my default.
If you need full control over checkout and subscriptions and you can handle the tax side, plain Stripe is still the most flexible thing there is.