Bug Reports
Open

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

Amritanshu Rai·16 days ago

Update after rebuilding this in my own app, in case it helps scope the fix.

I replaced the portal with payments.list plus invoices.payments.retrieve and it mostly works. The brand_id filter 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.retrieve only 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. But CustomerUpdateParams exposes 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_id on customerPortal.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.
  • An address argument on invoices.payments.retrieve, or any documented way to regenerate an invoice with a supplied address.
  • Billing address on the customer object, so one time buyers are not stuck and so an address belongs to a person rather than to a subscription.
  • A payment methods API, so the portal is not the only way to change a payment method.

Happy to test any of these. I have a live account with two brands and real payments on both, so I can verify quickly.

Posting anonymously