Customer portal is not brand aware: shows the primary brand's name and mixes billing history across brands
I have two brands under one business. A primary brand (a books store) and a
secondary brand (a developer education subscription). Products, payments and
subscriptions are correctly tagged with brand_id, and invoice PDFs are
correctly branded per brand, which is great.
The hosted customer portal is the one place that is not brand aware, and there
is no way to work around it from my side.
Two problems:
1. The portal always renders under the primary brand's name and logo. So a
customer who subscribed on my second brand clicks "manage billing" on that
site, and lands on a page titled with my books store. They have never heard of
that business. On a billing page this looks like a mis click or a phishing
page, which is the worst possible moment to confuse someone about who they are
paying.
2. Billing history is not filtered by brand. Dodo reuses one customer record
per email across the whole business, so a customer who has bought from both
brands sees both sets of payments mixed together in one list. It also quietly
reveals that the two sites are the same business, which is not something I want
to disclose.
Why I cannot fix this myself: customerPortal.create only accepts customer_id,
return_url and send_email. There is no brand_id argument, and no
business setting to scope it. Compare this to payments.list and
subscriptions.list, which both accept brand_id and work perfectly.
What I would like:
- customerPortal.create to accept an optional brand_id, which scopes both the
branding and the billing history to that brand
- or the portal to infer the brand from the subscription being managed
Related: webhook endpoints have the same shape of problem. I was told on
Discord that brand scoped webhook urls are being worked on. The portal feels
like the same gap in the same place.
For now I have had to rebuild billing history and invoice downloads in my own
app using payments.list with brand_id plus invoices.payments.retrieve, and drop
the portal link entirely. That works, except payment methods, which have no
public API at all, so there is no way to let a customer change their payment
method without sending them to a portal branded as a different business.
1 Comment
Sign in to comment

Update after rebuilding this in my own app, in case it helps scope the fix.
I replaced the portal with
payments.listplusinvoices.payments.retrieveand it mostly works. Thebrand_idfilter on payments.list does exactly what I need, and the invoice PDFs come back correctly branded per brand already. So the data layer is brand aware. It really is only the portal shell that is not.Two more gaps I hit on the way. Both mean the portal can still do things the API cannot, so "just build it yourself" is not a complete answer.
1. Invoices cannot be regenerated with a full address
The portal offers "Download with full address details", where the customer types in address line, state, city and postal code and gets an invoice with the complete address on it.
invoices.payments.retrieveonly takes a payment id, so there is no way to offer that outside the portal.This matters more than it sounds. My checkout uses
minimal_address, so I collect only postal code and country. Every invoice therefore reads "560024, IN". Anyone expensing the purchase at work needs a real address on the document, and today the only way to produce one is the portal, which is branded as my other business. So the customers most likely to need it are exactly the ones I least want landing there.2. The billing address is stored on the subscription, not the customer
subscriptions.update({ billing })works fine for subscribers. ButCustomerUpdateParamsexposes only email, name, phone_number and metadata. So a one time purchase, in my case a lifetime plan or a one year plan, has no address to edit at all. Those customers cannot complete or correct their billing address by any route, portal or API.It also means the address is per subscription rather than per person, so someone with two subscriptions has to fix it in two places, and there is no single source of truth for who they are.
What would help, roughly in priority order
brand_idoncustomerPortal.create, as in the original post. That alone unblocks everything, because I could simply link to the portal again and delete the code I just wrote.invoices.payments.retrieve, or any documented way to regenerate an invoice with a supplied address.Happy to test any of these. I have a live account with two brands and real payments on both, so I can verify quickly.