A mid-sized e-commerce company came to me with a problem I have seen dozens of times: their NetSuite ERP and PrestaShop webshop were operating in complete isolation. Over 200 employees, tens of thousands of orders per month, and yet every single order placed on the webshop had to be manually re-entered into NetSuite by the operations team.
The consequences were predictable. Order processing took an average of four hours from placement to ERP entry. Invoices were delayed because finance had to wait for orders to appear in NetSuite before they could bill. Inventory counts drifted between the two systems, leading to overselling on popular SKUs and phantom stock on discontinued items. The warehouse team was printing pick lists from one system while checking stock levels in another.
The company had tried a plugin-based integration before. It lasted three months before they turned it off. The data was unreliable, errors were silent, and nobody trusted the numbers. They needed something built for their actual operational reality, not a generic connector that assumes every business works the same way.
Why Batch Sync Fails at Scale
The most common approach to NetSuite-webshop integration is batch synchronization. A cron job runs every 15 or 30 minutes, pulls new orders from PrestaShop, transforms them, and pushes them into NetSuite. On paper, this sounds reasonable. In practice, it falls apart once you reach any meaningful volume.
The first problem is data freshness. A 15-minute sync interval means your inventory data is always at least 15 minutes stale. During a flash sale or promotional period, that window is enough to sell 50 units of a product you only have 20 of. The warehouse gets impossible pick lists, customers get cancellation emails, and your support team spends the next week handling complaints.
The second problem is error handling. When a batch of 200 orders fails because order number 47 has an invalid tax code, what happens to orders 48 through 200? Most batch implementations either fail the entire batch (losing all 200 orders) or skip the failed record and continue (losing order 47 silently). Neither outcome is acceptable when real money is involved.
The third problem is the processing lag itself. When you run a nightly import of product data from NetSuite to PrestaShop, any price change or stock adjustment made at 9 AM does not appear on the webshop until the next morning. For a company with over 15,000 SKUs and daily price adjustments based on supplier costs, this means the webshop is showing incorrect prices for up to 24 hours.
Finally, batch systems create an operational dependency on timing. If the 02:00 AM sync fails and nobody notices until 08:00 AM, you have six hours of missing data. Recovery means running the batch manually, hoping it does not duplicate the records that did make it through, and then reconciling the mess. I have seen companies assign a full-time employee just to babysit their nightly sync jobs.
The Event-Driven Architecture
The architecture I built for this client replaces polling with events. Instead of asking "are there new orders?" every N minutes, the system reacts the moment something happens. When a customer places an order on PrestaShop, an event fires immediately. That event triggers a chain of operations that ends with a fully created sales order in NetSuite, typically within two to three seconds.
The flow works like this:
- A customer completes checkout in PrestaShop. A webhook fires to our middleware endpoint with the full order payload.
- The middleware validates the payload, assigns a unique idempotency key derived from the PrestaShop order ID, and writes the event to a message queue.
- A consumer picks up the message, transforms the order data into the NetSuite schema (mapping PrestaShop product IDs to NetSuite internal IDs, resolving customer records, applying the correct tax codes and currency), and submits it via the NetSuite REST API.
- On success, the middleware updates the PrestaShop order status and writes a sync confirmation record.
- On failure, the message is retried with exponential backoff: first retry after 5 seconds, then 30 seconds, then 2 minutes, then 10 minutes. After five failed attempts, the message moves to a dead letter queue for manual inspection.
Idempotent Message Handling
The most critical design decision in this architecture is idempotency. Webhooks are inherently unreliable. PrestaShop might fire the same webhook twice. Our middleware might crash after processing the order but before acknowledging the message. The network between us and NetSuite might timeout after NetSuite has already created the record.
Every message in the system carries an idempotency key. Before processing any order, the consumer checks whether that key has already been successfully processed. If it has, the message is acknowledged and discarded. This is implemented as a simple lookup against a processed_events table with the idempotency key as a unique index. The check and the subsequent write happen within the same database transaction.
This sounds straightforward, but getting it right requires discipline. The idempotency check must happen at the NetSuite submission layer, not at the queue consumer layer. The reason: if the consumer processes the message and writes to NetSuite, but crashes before marking the event as processed, the next attempt will correctly identify that the event has not been marked as done, and it must be safe to retry. The NetSuite submission itself uses a combination of the external ID field and a pre-submission check to ensure no duplicates are created in the ERP.
The Dead Letter Queue
Failed messages that exhaust their retry budget end up in a dead letter queue. This is not a place where messages go to die. It is an operational tool. Every morning, the operations team receives a summary of any messages in the DLQ. Each entry includes the original payload, the error from each retry attempt, and a one-click option to resubmit after the underlying issue is fixed.
In practice, DLQ entries fall into a few categories: a new product was added to PrestaShop but not yet created in NetSuite, a customer's shipping address triggered a tax calculation error, or NetSuite was undergoing maintenance during a retry window. All of these are fixable without engineering intervention, which is the point.
The Hard Parts Nobody Tells You About
NetSuite API Rate Limits
NetSuite's REST API enforces a concurrency limit of 10 simultaneous requests per account for most license tiers. This is not 10 requests per second -- it is 10 concurrent connections. If your middleware fires 20 order submissions simultaneously during a traffic spike, half of them will get a 429 Too Many Requests response.
We handle this with a token bucket rate limiter in the middleware layer. The bucket is configured to allow 8 concurrent requests (leaving headroom for other integrations and manual API usage). Requests that cannot acquire a token are queued internally and processed as tokens become available. During Black Friday, this queue depth reached 340 messages, but every single one was processed within 12 minutes. Without the rate limiter, we would have seen cascading failures as retries compounded the overload.
PrestaShop Webhook Reliability
PrestaShop's webhook system is best described as "best effort." Webhooks can fail silently if the PrestaShop server is under load. There is no built-in retry mechanism. There is no delivery guarantee. If your middleware endpoint returns a 500 error, PrestaShop will not retry the webhook.
To compensate, we run a lightweight reconciliation poll every 5 minutes that checks for PrestaShop orders that have not appeared in the middleware's event log. This is not the primary sync mechanism -- it is a safety net. In a typical month, it catches between 5 and 15 missed webhooks out of roughly 30,000 orders. That is a 99.95% webhook delivery rate, which sounds good until you realize those 15 missed orders represent real customers who would not get their shipments without the reconciliation layer.
Currency and Tax Mapping
This client sells across multiple European markets. PrestaShop handles tax using tax rules tied to customer groups and zones. NetSuite uses tax codes tied to subsidiaries and nexus configurations. These two models do not map one-to-one.
We built a tax mapping table that resolves the PrestaShop tax rule ID to the correct NetSuite tax code based on the combination of customer country, product category, and the selling subsidiary. This table has 140 entries. Maintaining it is an ongoing operational task, especially when VAT rates change (which happens more often than you would expect in the EU).
Partial Failure Handling
The nastiest category of bugs in integration work is partial failures. An order syncs to NetSuite successfully, but the payment record fails because the payment method mapping is missing. Now you have a sales order in NetSuite with no associated payment, and the finance team's reconciliation reports are off.
Our approach is to treat order creation and payment application as a single atomic operation at the business logic level. If the payment application fails, the sales order is rolled back. This means the entire order goes to the retry queue rather than creating an inconsistent state in NetSuite. The alternative -- leaving partial records and trying to fix them later -- leads to a reconciliation nightmare that compounds over time.
The Reconciliation Layer
Even with event-driven sync, idempotent processing, and retry logic, drift happens. A NetSuite user manually edits an order. A PrestaShop plugin modifies a record outside the integration flow. A database restore rolls back a few minutes of transactions.
The reconciliation layer runs hourly and compares order totals, inventory quantities, and customer records between both systems. It does not fix discrepancies automatically. It flags them for human review with enough context to determine which system has the correct data. In the first month after launch, it flagged 23 discrepancies. By month six, that number was down to 2 or 3 per month, almost always caused by manual edits in NetSuite.
Infrastructure Decisions
The middleware runs on Google Kubernetes Engine (GKE). The choice was driven by three factors: the client was already on Google Cloud Platform for their other infrastructure, GKE gives us horizontal pod autoscaling out of the box, and Kubernetes' rolling deployment model means we can ship updates without any sync downtime.
The middleware itself is a set of small, focused services: an ingestion service that receives webhooks and writes to the message queue, a transformer service that handles data mapping, a submission service that talks to the NetSuite API, and a reconciliation service that runs on a schedule. Each service scales independently. During normal operation, we run two replicas of each. During peak periods like Black Friday, the ingestion and submission services scale to eight replicas automatically based on queue depth.
Monitoring
We use Prometheus for metrics collection and Grafana for dashboards and alerting. The key metrics we track are: messages in queue (should be near zero during normal operation), message processing latency (p95 should be under 5 seconds), dead letter queue depth (should be zero), and the reconciliation discrepancy count.
Alerting is configured in tiers. A queue depth above 50 sends a Slack notification. A queue depth above 500 pages the on-call engineer. Any DLQ message triggers an immediate notification to the operations team. These thresholds were tuned over the first three months based on actual traffic patterns.
CI/CD
Every push to the main branch triggers a pipeline that runs unit tests, integration tests against sandbox instances of both NetSuite and PrestaShop, builds a container image, and deploys to the staging environment. Production deployments are triggered manually after staging validation. The rolling deployment strategy ensures zero message loss during updates -- Kubernetes drains existing pods gracefully while new pods pick up queued messages.
Results and What I Would Do Differently
After going live, the results were immediate and measurable:
- Order processing time went from an average of 4 hours (manual re-entry) to under 5 seconds (automated sync).
- Inventory accuracy improved from approximately 91% to 99.7%, effectively eliminating overselling.
- Finance reconciliation went from a 2-day manual process at month-end to a 15-minute review of the automated reconciliation report.
- Operations headcount: three employees who had been doing full-time data entry were reassigned to customer service and vendor management roles.
The system has been running in production for over a year now. It has processed more than 360,000 orders without a single duplicate or lost record. It survived two Black Friday events, a NetSuite scheduled maintenance window that lasted 4 hours (the retry queue absorbed everything and drained within 20 minutes after NetSuite came back), and a PrestaShop server migration.
What I Would Change
If I were starting this project again, I would invest in structured logging from day one. We started with plain text logs and migrated to structured JSON logging in month three. Those first three months of debugging were significantly harder than they needed to be. When you are troubleshooting why a specific order failed to sync at 03:00 AM, you need to be able to query logs by order ID, customer ID, error type, and time range. Structured logging makes that trivial. Plain text logs make it an exercise in regex and frustration.
I would also set up distributed tracing (OpenTelemetry) from the beginning rather than adding it retroactively. Being able to follow a single order's journey from PrestaShop webhook to NetSuite submission, with timing data for each step, is invaluable for both debugging and performance optimization.
Dealing with disconnected systems? I have built dozens of ERP integrations -- NetSuite, SAP, Microsoft Dynamics, connected to PrestaShop, WooCommerce, Shopify, and custom platforms. If your order data lives in two places and neither one is right, let's talk about yours.