Running 6 Marketplaces From One Panel: An Approval Gate for Every Outbound Operation
The multichannel e-commerce system I built for a carpet brand brings Trendyol, Hepsiburada, N11, Amazon, Çiçeksepeti and Pazarama together in one panel, along with the brand's own website, the supplier's stock service and the e-invoicing provider. In a system like this, the real danger isn't an API going down. It's the system doing the wrong thing while working perfectly: zeroing the stock of a mis-matched variant, halving the price of 4,000 products because of a spreadsheet mistake, or placing the same order with the supplier twice. None of these throw an error. They just cost money. Here's how I guarded against them.
The rule: every outbound operation goes through the gate
One of the system's non-negotiable rules: pushing stock or prices to a marketplace, reporting order status, issuing an invoice, processing a refund, sending a shipping label over WhatsApp... every operation that changes the outside world goes through a single ApprovalService. Each operation type has a mode configurable from the panel: approval or automatic. The default is very conservative: only stock decrements run automatically, and everything else waits for a human to click "Approve".
public async Task<OperationMode> GetModeAsync(OperationType type, CancellationToken ct)
{
var policy = await db.OperationPolicies.AsNoTracking()
.FirstOrDefaultAsync(p => p.Type == type, ct);
// An operation type with no setting stays on the safe side: it waits for approval.
return policy?.Mode ?? OperationMode.Approval;
}The ?? on that small line is, in my view, the most important decision in the system. When a new operation type is added and nobody remembers to configure it, the default behavior is "wait", not "run". A safe default makes forgetfulness harmless.
In both modes the request and its outcome are recorded. An automatic operation is still written as an approval request; it's simply executed immediately with an IsAutomatic flag. So the answer to "what did we send to Trendyol last night?" always lives on the same screen.
Stale requests: never approving an outdated version
An approval queue creates a new problem of its own. Say a new price list is uploaded at 10:00 and a Trendyol price push is waiting for approval. At 11:00 the list is corrected and uploaded again. Now there are two requests in the queue, and if someone approves the old one, the wrong prices go out.
To handle this, each request has an optional scope key (ScopeKey), for example price:trendyol:0. When a new request opens with the same key, older pending requests are moved to Superseded and can no longer be approved. When large pushes are split into chunks (price:trendyol:0, :1, :2...), prefix-based superseding drops all the old chunks at once. The approver always sees the latest version.
Stock only flows through approved mappings
Who decides that a product on a marketplace and a variant in the catalog are the same thing? During the initial import the mapping is suggested automatically, but no stock flows until it's approved. The system never touches the stock of a variant whose mapping hasn't been approved. That eliminates the "auto-matching caught a false similarity and zeroed another product's stock" scenario entirely.
Pricing follows the same conservatism: each channel's prices come from its own Excel list. If a product isn't in the list, its current marketplace price is left alone and it can't be published to that channel for the first time; it shows up as an error on the push screen. When I ran a read-only trial with a sample Trendyol list, 4,208 of 4,830 variants matched. The 40 ambiguous ones came from two separate rows with the same name in the list. Instead of guessing, the system flagged them as ambiguous, which was exactly the right behavior.
Staying safe when the dev environment is wired to production
The project had a practical reality: the development environment was connected to the live database so I could work against real orders and products. That meant an accidental click locally could mean writing to a real marketplace.
I solved this at the code level: when Integrations:ReadOnly=true is set, every channel client throws a ReadOnlyModeException before it performs any write. Pushing stock/prices, order notifications, cancellations, labels, invoice uploads, invoice issuing, refunds, WhatsApp and email: all of them sit behind that check. Scheduled jobs are disabled in development too. Putting this inside the clients instead of relying on "I'll be careful" discipline makes the late-night mistake impossible.
One channel's failure doesn't stop the others
Six marketplaces means six different failure personalities. One goes into maintenance at night, one returns the occasional 5xx, one slows down at peak hours. Order fetching and stock sync are isolated per channel: a failure in one is logged and recorded in the activity log, but it doesn't cut the rest of the run short. On transient 5xx errors, only read requests are retried. Blindly repeating write requests would mean a duplicate operation if the first one had actually succeeded.
A supplier without an API: a bot, but with strict rules
Stock lives with the supplier, and the supplier's dealer site has no ordering API. Orders are placed by a separate Playwright bot. Since real money and real shipments are involved, the bot's rules are strict: duplicate-order prevention via a recorded order number, stop if the cart isn't empty, one order at a time, select and verify the address by the order number, one final check right before confirming. If anything goes wrong, the bot doesn't guess. It takes a screenshot and drops the job onto a "needs manual review" list. If the supplier ever opens an API, only that layer will change, because the bot also sits behind an interface.
Wrapping up
In multichannel integration, "automating everything" is the easy part. The hard part is making it impossible for the automation to do the wrong thing. An approval gate with a safe default, scope keys that drop stale requests, no stock without an approved mapping, a code-level read-only mode, and channels that can't take each other down: these decisions made the system a little "slower" but far more trustworthy. Details of the panel, storefront and e-invoicing side are on the project page.