From “Seller Pending” to a Real Stripe Checkout Flow in MyZubster
Tonight I worked on one of the most important parts of the MyZubster marketplace: turning the Seller membership from a manual payment request into a real Stripe subscription flow.
The goal was simple in theory:
Marketplace → Become Seller → Stripe Checkout → subscription → webhook → Seller activation
In practice, the work exposed several integration issues that are exactly the kind of problems that only appear when you push a feature all the way to production.
We started from an existing Seller membership model priced at €9.90/month. Buyers can use the marketplace for free, while publishing listings requires an active Seller membership.
The backend already had the key building blocks:
POST /api/marketplace/seller/checkout
POST /api/marketplace/seller/webhook
GET /api/marketplace/seller/plan
GET /api/marketplace/seller/me
The webhook flow is designed to update Seller access from real Stripe subscription events, including:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.paid
invoice.payment_failed
Webhook signatures are verified from the raw request body, and Seller access is suspended or activated according to the real Stripe subscription state.
The first problem we found was in the frontend.
The Marketplace UI was still using the old manual flow:
/api/marketplace/seller/subscribe
So the “Become Seller” button created a pending membership but never sent the user to Stripe.
We replaced that flow with:
const payload = await apiAction(
'/api/marketplace/seller/checkout',
{}
);
window.location.assign(payload.checkoutUrl);
That uncovered the next bug.
The backend returned:
{
checkoutUrl: session.url
}
while the frontend initially expected:
payload.url
That mismatch was fixed in a second small PR.
After that, the browser was finally reaching the Stripe API, which exposed the next production issue:
No such price: 'price_...'
The Stripe Price itself existed, was active, was in live mode, and represented:
MyZubster Seller
€9.90
EUR
monthly recurring
But the production credentials and Price context were not resolving consistently.
Rather than keep coupling Checkout to a pre-created Stripe Price ID, we changed the design.
The Seller Checkout now creates the recurring price inline using Stripe price_data:
'line_items[0][price_data][currency]': 'eur',
'line_items[0][price_data][unit_amount]':
String(Math.round(amount * 100)),
'line_items[0][price_data][recurring][interval]':
'month',
'line_items[0][price_data][product_data][name]':
'MyZubster Seller',
'line_items[0][quantity]': '1'
This means the business rule remains controlled by MyZubster:
Seller Membership
€9.90/month
but Checkout no longer depends on a specific price_... object being visible to the Stripe credential used by production.
We also changed the Stripe configuration check so that the Seller Checkout requires the actual credentials it needs rather than an external Price ID.
The work was split into small PRs:
903
Connect Seller CTA to Stripe Checkout
904
Use checkoutUrl returned by the backend
905
Create Seller subscription price inline with Stripe price_data
For each change we kept the scope narrow and ran the project CI before merging.
Tonight’s latest Seller backend change passed:
Security Audit ✅
CI – Test e Lint ✅
Continuous Evidence Gate ✅
The targeted Seller membership tests also passed locally.
One lesson from this session is that getting payments “implemented” is very different from getting payments operational in production.
The integration had to survive every boundary:
React UI
↓
authenticated Marketplace API
↓
Stripe Checkout Session
↓
Stripe subscription
↓
signed webhook
↓
SellerMembership
↓
listing authorization
Each boundary exposed a different class of bug: an outdated frontend action, a response-field mismatch, environment configuration, and finally coupling to an external Stripe Price object.
The resulting architecture is now simpler.
MyZubster still does not treat a button click as payment confirmation. Seller access becomes valid only from actual payment/subscription state propagated through Stripe.
That distinction matters.
The Marketplace also remains non-custodial. Wallet addresses can be displayed for ecosystem interactions, but MyZubster does not store wallet seeds or private keys and does not sign blockchain transactions on behalf of users.
The next milestone is the real end-to-end production verification:
open Checkout → complete payment → receive signed webhook → activate Seller → publish the first paid Seller listing.
That will be the point where the marketplace moves from “payment integration” to verified recurring revenue.
Building in public means showing this part too: not just the green architecture diagram, but every little mismatch between UI, backend, credentials, deployment configuration and external APIs that has to be eliminated before a real customer can pay.
And tonight, MyZubster got significantly closer.
Top comments (0)