If you're building a store — payment integration is a few hours of work. If you're building a marketplace — it's a few weeks, rewriting modules, and a real risk that the payment operator will simply refuse to cooperate.
This isn't clickbait. It's an architectural and legal difference that can sink a project at the stage when everything seems to be working. And it is one of the questions you should ask before starting e-commerce shop.
Part 1 — Store: Simple Model, Simple Integration
In a classic store, the payment flow looks like this:
Customer → pays → money goes to you
One merchant, one recipient, zero splits. There's no talk of payout orchestration, marketplace compliance, or seller KYC. It's a simple transaction — and that's exactly why the integration is straightforward.
Stripe in Poland — The Current State
Stripe today supports a full set of payment methods in Poland:
- Przelewy24 (as a payment method via Stripe)
- BLIK
- Cards (Visa, Mastercard, Amex)
- Apple Pay / Google Pay
- Klarna and other BNPL
- Bank transfers
For the vast majority of stores, one provider is enough. PayU and Przelewy24 as standalone integrations still make sense — they serve the same role as payment method aggregators — but at lower GMV it's hard to justify the additional maintenance cost.
Medusa.js — Stripe Integration
Medusa.js provides an official plugin:
@medusajs/payment-stripeConfiguration in medusa-config.ts:
{
resolve: "@medusajs/payment-stripe",
options: {
api_key: process.env.STRIPE_API_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
},
}Under the hood, Medusa creates a PaymentIntent when initializing checkout:
await stripe.paymentIntents.create({
amount: cart.total,
currency: "pln",
automatic_payment_methods: {
enabled: true,
},
});The automatic_payment_methods: true flag lets Stripe dynamically select available payment methods based on the customer's location and account configuration — in Poland this means BLIK and P24 appear automatically.
Why It Takes Just a Few Hours
Because the entire flow is:
Customer → checkout → Stripe → webhook → order paid
And that's it. One merchant, one recipient, no splits, no payout orchestration. The payment_intent.succeeded webhook hits the backend, Medusa marks the order as paid. Done.
When PayU or Przelewy24 Make Sense
At high GMV — above a few million PLN per month — it's worth talking to PayU or P24 about custom rates. Enterprise support, a dedicated account manager, room to negotiate commissions. But: higher integration maintenance cost, separate documentation, separate webhooks, separate test environments. For an early-stage startup, this is rarely a justified trade-off.
Part 2 — Marketplace: This Isn't a Payment, It's a Financial System
The marketplace model looks different:
Customer → pays → platform → sellers
And it's that middle step — the platform — that changes everything. When the customer's money first comes to you and then you distribute it among sellers, you stop being a regular store. You become an entity processing financial funds on behalf of third parties. This has legal, compliance, and technical consequences.
What Changes Compared to a Store
- Multiple recipients — each transaction may go to a different seller (or several)
- Platform commissions — you need to cut your share before paying out
- Payouts — payout schedules, fund holds, linkage to refunds
- KYC/AML — you must verify seller identities
- Compliance — depending on the model, you may need a payment institution license
In baseline implementation it looks like this:

The Problem with Polish Operators
This is where the real wall begins. PayU and Przelewy24 offer marketplace products (split payments, subaccounts), but their onboarding processes are designed for large, established companies. Documentation requirements, verification time, lack of support for early-stage startups — this isn't theory.
A concrete case: a PayU marketplace module was built, several weeks of integration, sandbox environment testing — and then a refusal to cooperate at the production stage. Project scrapped, timeline in shambles. This isn't an exception. It's a pattern.
Stripe Connect as the Solution
Stripe Connect solves this problem by shifting compliance onto itself. Stripe verifies sellers (KYC), handles AML, holds funds, and manages payouts. You as the platform don't need to be a payment institution — Stripe is one on your behalf. For a marketplace in Poland, this is often the only realistic option at an early stage.
Account Types in Stripe Connect
Standard — the seller has their own Stripe account and manages their own settings. Least control on the platform side, but also least responsibility.
Express — the optimal choice for most marketplaces. Stripe provides a ready-made onboarding UI, handles KYC, but the platform retains control over payouts and branding is partially yours. The seller sees a simplified dashboard.
Custom — full control, full responsibility. You build your own onboarding, your own dashboard, your own UX. Stripe works in the background as the engine. More expensive, more complex, justified at large scale.
For 90% of marketplaces launching in Poland: Express.
Payment Models in Stripe Connect
Destination charges — payment goes to the platform, Stripe automatically transfers a portion to the seller. Simple implementation, but the platform is the party to the transaction vis-à-vis the customer.
await stripe.paymentIntents.create({
amount: total,
currency: "pln",
application_fee_amount: platformFee,
transfer_data: {
destination: sellerStripeAccountId,
},
});Separate charges and transfers — payment goes to the platform, transfer to the seller is a separate operation. More flexibility (you can transfer to multiple accounts, delay the transfer, link to fulfillment), but also more code to write and more webhooks to handle.
When to use which? Destination charges work well with a simple model of 1 order = 1 seller. Separate charges and transfers — when you have a cart with products from multiple sellers, complex commissions, or want to tie the transfer to order status.
Delayed Payouts — Holding Funds
Stripe Connect lets you configure payout schedules at the seller account level. You can set:
- Payout after N days from transaction (e.g. 7 days — time to handle refunds)
- Manual payout — the platform triggers the payout manually or programmatically
- Automatic payout — Stripe pays out according to the default schedule
Delayed payouts aren't just convenience — they're a risk management mechanism. If a customer files a refund, you have time to reverse the transfer before funds leave the platform.
Webhooks in Stripe Connect
In the Connect model you must handle webhooks for both the platform account and connected accounts. Key events:
account.updated— change in seller onboarding status (e.g. KYC completion)payment_intent.succeeded— payment confirmationtransfer.created— transfer to sellerpayout.paid/payout.failed— payout status to selleraccount.application.deauthorized— seller disconnected their account
Webhooks for connected accounts require passing the Stripe-Account header when verifying the signature — otherwise verification will fail.
At Artovnia marketplace, implementation could be represented with this simplified diagram:

Part 3 — Critical Details That Hurt in Production
Refunds in a Marketplace
A refund in a store is a simple operation: you reverse the PaymentIntent, Stripe returns the money to the customer. In a marketplace it's a cascade:
- Reverse the transfer to the seller (or create a reversal)
- Refund the money to the customer
- Handle the platform commission (do you refund it?)
If the funds have already reached the seller and been paid out to their bank account, Stripe cannot automatically reverse them. You need a settlement mechanism with the seller — either deduction from future payouts or manual collection. This isn't an edge case — it's a scenario you must design before launch.
Idempotency Keys
Every request to the Stripe API that creates or modifies a resource should have an idempotency_key. It's a string that Stripe remembers for 24 hours — if you send the same request twice with the same key, Stripe returns the result of the first call without creating a duplicate.
await stripe.paymentIntents.create(
{
amount: total,
currency: "pln",
application_fee_amount: platformFee,
transfer_data: { destination: sellerStripeAccountId },
},
{
idempotencyKey: `order_${orderId}_payment`,
}
);Without idempotency keys, retry logic on network timeouts can result in double charges. In a marketplace where one checkout can trigger multiple operations (PaymentIntent + transfer + payout schedule), this is not optional.
Webhooks as Source of Truth
Never rely on a frontend response as payment confirmation. The customer may close the browser, lose connection, get a JS error — and the payment may have gone through anyway. The only reliable source of truth is the payment_intent.succeeded webhook received by the backend.
Practical consequences:
- Backend must be idempotent on webhooks (the same event may arrive multiple times)
- You must verify the webhook signature (
stripe.webhooks.constructEvent) - Event order is not guaranteed — design a state machine, not a sequence
Environments: Test, Live, Connect
Stripe has separate API keys for test and live environments — that's obvious. Less obvious is that in Stripe Connect, each connected account has its own keys and its own environment. Test connected accounts don't work with live platform keys and vice versa. When onboarding sellers in a test environment, use Stripe test accounts (available via the dashboard) — you can't test the full KYC flow in production without real data.
Glossary — For less technical readers
PaymentIntent
PaymentIntent is an object representing the intent to collect a payment from a customer. It's not the payment itself — it's its lifecycle manager. Stripe designed it for asynchronous payment methods: BLIK requires confirmation in a banking app, 3DS requires user interaction, bank transfers can wait for hours. PaymentIntent tracks state throughout.
States you must handle:
requires_payment_method— customer hasn't selected a payment method yetrequires_action— additional action required (3DS, BLIK confirmation)processing— payment in progress (typical for BLIK and transfers)succeeded— payment completed successfullycanceled— canceled (by customer or timeout)requires_capture— for payments with manual capture (e.g. pre-authorization)
In Medusa.js, the PaymentIntent state is synchronized via webhooks — you don't poll the API.
Idempotency Key
A string passed in the Idempotency-Key header to the Stripe API. Stripe remembers the result of a request with a given key for 24 hours. Subsequent requests with the same key return a cached response without executing the operation. The key should be unique per operation, not per session. Good pattern: {entityType}_{entityId}_{operation} — e.g. order_abc123_payment_create.
Webhook
An HTTP POST sent by Stripe to your endpoint when a resource's state changes. Stripe guarantees delivery (with retry), but does not guarantee ordering or exactly-once delivery. Every webhook contains a signature in the Stripe-Signature header — always verify it, without exception. An unverified webhook is a potential attack vector. In Stripe Connect, webhooks can come from the platform account or from a connected account. The latter contain an account field in the payload — you must distinguish them in your routing logic.
Stripe Connect
A system for managing seller accounts and the flow of funds between them. Stripe Connect is not a plugin — it's a separate architectural layer that: handles seller onboarding and KYC (Stripe collects and verifies data), holds funds on behalf of sellers, manages payouts to bank accounts, and shifts compliance responsibility to Stripe. For a marketplace in Poland, this means you don't need to apply for a payment institution license — Stripe is a licensed institution and acts as an intermediary. This isn't a detail — it's the legal foundation of the entire platform model.
Summary — A Decision You Make Once
Stripe Connect isn't a "convenient option" or a shortcut. It's an architectural decision that determines how you build seller onboarding, how you design the payment flow, how you handle refunds, and how you'll scale.
It's also a legal decision. Without Stripe Connect or a similar solution (Mangopay, Adyen for Platforms), you must handle compliance yourself — KYC, AML, fund storage, reporting. In Poland, that means either a payment institution license or working with a licensed partner. None of the Polish operators make this easy for startups.
And finally — it's often the only realistic option for a marketplace in Poland at an early stage. Not because Stripe is the best at everything. Because the alternatives either don't support the marketplace model or require scale and resources a startup doesn't have.
If you're building a marketplace and wondering whether "it can be done more simply" — the answer is: no. But it can be done smartly. And that's exactly the difference.
You can use my experiance in building e-commerce platforms and book a free consultation.


