Engineering / NetSuite integrations

Building a Production-Grade NetSuite–ShipStation Integration

Our client runs Shopify into NetSuite through an iPaaS platform they want to move off. The ShipStation build was step one: a native SuiteScript integration that pushes orders on pack, prevents duplicates, and writes tracking and real shipping cost back, with no external platform in the path. This is how it works, and what it proved.

The integration does two things. When a warehouse user packs an item fulfillment in NetSuite, the order is pushed to ShipStation in real time. When ShipStation ships it, tracking, package details and the actual shipping cost flow back into NetSuite and the fulfillment closes automatically. The rest of this post is everything those two sentences hide.

WHYStep one off the iPaaS platformThe client wants to retire their middleware. The shipping leg was the first flow to move.
OUTReal-time push on packA User Event fires the moment a fulfillment reaches Packed status. No batch delay for the warehouse.
INTracking writebackA Map/Reduce retrieves shipments, stamps tracking and cost, and flips the fulfillment to Shipped.
CONFIGZero hardcoded credentialsTokens, endpoints and behavior flags live on a custom record. Admins change them without a deployment.
SAFEIdempotent by designDuplicate protection, loop protection, and error handling that never blocks a fulfillment save.

Project context

The client's storefront pipeline already existed and already worked: Shopify orders flowed into NetSuite through an iPaaS platform. What was missing was the shipping leg, getting packed fulfillments into ShipStation and shipment results back out.

The obvious answer was to build the shipping flow on the platform they already had. They asked us not to. The client's direction is to move off that platform entirely, retiring the license, the monitoring and the second set of deployment mechanics, and consolidating integration logic inside NetSuite where their team already works. Adding a new flow to a system they intend to decommission would have meant building it twice.

So ShipStation became the first flow to move. It was a deliberate choice: a self-contained integration with a clear boundary, real operational stakes, and no dependency on the Shopify pipeline. If native SuiteScript could carry it, the same pattern could carry the rest. The build runs entirely inside NetSuite: a User Event script on the item fulfillment, a scheduled Map/Reduce for tracking retrieval, and a shared manager module that owns configuration and the ShipStation client.

System architecture: Shopify through an existing iPaaS platform into NetSuite, native SuiteScript to ShipStation and backThe existing pipeline flows from Shopify through an existing iPaaS platform into NetSuite and was untouched by this project. Inside NetSuite, an item fulfillment reaching Packed status triggers a User Event script that pushes the order to ShipStation. A scheduled Map/Reduce retrieves shipment data from ShipStation and writes tracking and cost back onto the fulfillment, flipping it to Shipped. Shopify iPaaS platform NetSuite Existing integration, untouched by this project Item fulfillment Reaches Packed status User Event script This build, real time ShipStation Order created on pack Map/Reduce Retrieves tracking and cost Shipments endpoint, fulfillments fallback Back in NetSuite Tracking, cost, status Shipped Everything below the top row runs inside NetSuite as SuiteScript, with no external platform
The Shopify pipeline still runs on the iPaaS platform. The shipping leg was the first flow moved to native SuiteScript inside NetSuite.
Design goals
  • No dependency on the iPaaS platform the client is retiring
  • Push fulfillments to ShipStation immediately after packing
  • Prevent duplicate orders under retries and re-saves
  • Support any number of storefronts from one codebase
  • Never block warehouse users, regardless of what the API is doing
  • Write tracking and actual shipping cost back into NetSuite automatically

Everything else in this post exists because of those six goals.

01

Configuration is data, not code

Every connection detail lives on a dedicated integration configuration record: the API token, the endpoint, and a free-form JSON settings blob for behavior flags. Credentials never touch the codebase, and an administrator can change an endpoint or toggle a behavior without anyone deploying a script.

The config record is also what makes the integration multi-store. On initialization, initializeBySite() looks up the correct ShipStation configuration by matching the Shopify site stamped on the fulfillment, combined with a system-type filter. One codebase serves multiple storefronts, each with its own ShipStation account and its own rules. Adding a brand means adding a record, not forking a script.

Because the User Event fires on every fulfillment save, config lookups would burn governance units fast if they hit the database each time. So configurations are cached in memory per system ID, with an explicit forceRefresh escape hatch for the cases where you genuinely need a fresh read.

Why this matters
See also  NetSuite: SMS/Voice Call No Longer Supported for 2FA Setup

Configuration stored as data instead of code means administrators change endpoints, credentials and business rules themselves, immediately, without a deployment cycle or a developer in the loop. Scope changes become routine admin work instead of change requests.

02

Designing the outbound sync

The User Event script runs on CREATE, EDIT and PACK, and pushes to ShipStation's createorder endpoint the moment a fulfillment reaches Packed status. For the warehouse, that means no waiting on a scheduled synchronization job: an order is available in ShipStation, ready for a label, the moment packing is complete.

Outbound eligibility gate for the ShipStation pushFour conditions checked before a fulfillment is pushed to ShipStation: status is Packed, not already synced, customer matches the configuration, and the sales order location matches the configuration. Only fulfillments passing all four are sent to the createorder endpoint. Item fulfillment saved ELIGIBILITY GATE 1  Status is Packed 2  No ShipStation ID on the record yet 3  Customer matches the configuration 4  Sales order location matches the configuration Push to createorder Real time, on pack Conditions 3 and 4 are config-driven: scope changes are data changes, not deployments
A single predicate decides whether a fulfillment syncs. Customer and location scoping come from the config record.

Not every fulfillment should go to ShipStation, so a single eligibility predicate gates the push. It checks four conditions: the fulfillment's status is Packed, it has not already been synced, the customer matches the configuration, and the sales order's location matches the configuration. The last two are config-driven, which means changing which customers or locations are in scope is a data change an admin makes, not a code change we deploy.

What actually goes in the payload

The order that lands in ShipStation carries enough context that nobody has to open NetSuite to figure out what it is.

Built into every createorder payload
Bill-to and ship-to addressesResidential flagSKU, title and UPC per lineLine keys for round-trip traceabilityPackages with weight and dimensionsShip-by dateRequested shipping serviceeTail order ID and Class
ONE ITEM SEARCH, NOT ONE PER LINE
ShipStation order that reconciles back to NetSuite line by line

Addresses are built properly. Both billTo (from the sales order) and shipTo (from the fulfillment) come from NetSuite address subrecords, including the attention versus addressee split, street 2, and phone. The residential indicator rides along too, and that one is not cosmetic: it directly affects carrier rating accuracy.

Items are enriched in one search. Line items are collected first, then resolved in a single item search rather than a lookup per line. Each line ships with its SKU, display title, UPC, and a lineItemKey tied back to the NetSuite order line, so anything ShipStation reports can be traced to the exact line it came from.

Matrix SKUs are normalized. NetSuite matrix item IDs carry the parent prefix. The integration strips everything up to the last colon, so ShipStation receives the clean child SKU the warehouse actually scans.

Unit price is a policy decision, so it is a setting. A use_declared_value flag toggles between the item's declared value and the sales order rate, with a fallback to amount divided by quantity when the rate is blank. Declared value matters for customs declarations and insured shipments; making it a setting means each storefront picks its own policy.

Packages are a manifest, not an afterthought. Packages are built from the fulfillment's package sublist with weight (defaulting to 1 lb), dimensions, insured value, a content description, and a nested product list with descriptions truncated to ShipStation's 100-character limit so the API never rejects a long item name.

Deadlines and reconciliation context travel with the order. The ship-by date on the fulfillment maps to ShipStation's shipByDate, and the NetSuite ship method's display text becomes requestedShippingService. The eTail order ID and the sales order's Class are pushed into advancedOptions.customField1 and customField2, so shipping labels and ShipStation reports carry the business context needed to reconcile back to NetSuite.

03

Synchronizing shipment data back to NetSuite

A scheduled Map/Reduce handles the return trip. It finds fulfillments that were pushed but not yet shipped, asks ShipStation what happened to them, and writes the answer back.

Two-source tracking retrieval with fallbackThe Map/Reduce first queries the ShipStation shipments endpoint. If nothing is found it falls back to the fulfillments endpoint, covering both labels bought in ShipStation and third party or dropship fulfillments recorded there. Field mapping differs per source. Confirmed tracking is written back to NetSuite and the fulfillment status flips to Shipped. Map/Reduce candidate search Paginated in 1,000-record batches Query /shipments Labels bought in ShipStation nothing found Fall back to /fulfillments Third party and dropship Field mapping differs per source: serviceCode / fulfillmentServiceCode shipmentCost / fulfillmentFee Write back to NetSuite Tracking, weight, dims, carrier, cost Status flips to Shipped
One retrieval path covers labels bought in ShipStation and fulfillments recorded there by third parties.

Two sources, one answer. The Map/Reduce first queries /shipments. If nothing comes back, it falls back to /fulfillments, which covers both labels bought inside ShipStation and third-party or dropship fulfillments merely recorded there. The two endpoints do not speak the same dialect, so field mapping differs per source: serviceCode versus fulfillmentServiceCode, shipmentCost versus fulfillmentFee.

The carrier sublist trap. NetSuite exposes a different package sublist depending on the carrier configured on the record. Write to package when the record has packagefedex and the script fails. The integration detects which sublist actually exists and targets the right column names dynamically, so a carrier mismatch never kills a writeback.

See also  SuiteQL vs. Search.lookupFields: Performance compared

Units arrive clean. Weights returned in ounces are converted to pounds at two decimals, and dimensions are flattened into a readable LxWxH units string for the carton number field.

The fulfillment closes itself. Once tracking is confirmed, the job stamps tracking number, weight, dimensions, carrier code, service code and shipment cost onto the record, then flips shipstatus to Shipped. No one touches the fulfillment again.

For the rest of the business, this is the part that matters: customer service, finance and operations all read the same shipping data inside NetSuite, with no manual reconciliation between systems and no exporting from ShipStation to answer a question.

Why this matters

The actual shipment cost from ShipStation lands in a custom field on the fulfillment, which is what makes landed-cost and margin reporting possible inside NetSuite instead of in a spreadsheet next to it. Finance sees the real cost of every shipment where the books already live.

The candidate search that feeds all of this pages through results in 1,000-record batches until exhausted, so the job scales past NetSuite's single getRange ceiling rather than silently processing the first thousand and stopping.

04

Production considerations

Everything above describes the happy path. Integrations earn their keep on the other paths.

Idempotency and duplicate protection

When ShipStation accepts an order, its orderId and orderKey are written back to custom body fields on the fulfillment. Those fields then serve as a guard: a fulfillment that already carries a ShipStation ID is never re-pushed as a new order. When an eligible record is pushed again, the stored IDs are echoed in the payload, so ShipStation updates the existing order instead of creating a duplicate.

Loop protection

The writeback Map/Reduce saves fulfillment records, and saving a fulfillment fires the User Event. Without a guard, the tracking writeback would trigger another outbound push. So the User Event short-circuits when runtime.executionContext is MAPREDUCE, and the loop never starts.

Cancellation and re-sync, built but switched off

The codebase includes two operator conveniences. A cancellation routine calls ShipStation's cancel endpoint and, on an HTTP 204, clears the stored ShipStation ID so the record becomes eligible for a fresh sync. And a re-sync checkbox lets an operator force a re-push, clearing itself on success so it cannot be left set by accident.

Both are disabled in this implementation. The client's workflow did not need them, and an integration should not carry live switches nobody uses. They stay in the codebase, tested and ready to enable the day the workflow changes.

Errors never block the warehouse

Every API call logs its method, URL and raw response body at audit level, so when something goes wrong the evidence is already in the execution log. The User Event wraps its work in try/catch so an integration failure never prevents a warehouse user from saving a fulfillment. The Map/Reduce isolates errors per record, so one bad fulfillment does not kill the batch behind it.

The design principle underneath all of it

The warehouse keeps moving no matter what the integration is doing. Failures are logged, retried or flagged for an operator. They are never allowed to stand between a packer and a saved fulfillment.

Results

The integration has been live in production since go-live, with no iPaaS platform in the path, running two storefronts on an architecture that does not care how many there are.

SCOPEStorefronts are records, not codeTwo live today. Onboarding another is a configuration record, with no development work in the path.
EFFORTManual tracking entry eliminatedFulfillments close themselves. Nobody copies tracking numbers between systems.
FINANCEShipping cost reconciled automaticallyActual per-shipment cost lands in NetSuite, where margin reporting already happens.
STACKOne flow off the platformShipping no longer depends on the middleware the client is working to retire.
DATANo duplicate ordersIdempotency holds under retries, re-saves and the tracking writeback loop.
UPTIMEThe warehouse never waitsNo fulfillment save has ever been blocked by an integration failure.

The operational change is the one people notice first. Before, shipping data lived in two places and someone had to move it. Now a packer saves a fulfillment and the order is in ShipStation; a label gets bought and the fulfillment closes itself with tracking, weight, dimensions, carrier, service and cost already stamped on it.

See also  Unlock Your NetSuite Certification: The Ultimate Guide

The downstream effect is quieter but larger. Customer service answers "where is my order" from NetSuite. Finance sees real shipping cost per shipment against the revenue on the same record. Operations reports on carrier and service mix without exporting anything. All three read the same data, because there is only one copy of it.

And because scope is configuration rather than code, the second storefront went live without a new build. Nothing in the design caps the number: site resolution reads whichever storefront is stamped on the fulfillment and loads that configuration, so the tenth behaves exactly like the first.

The strategic result is the one that matters most to this client. Shipping is now a flow their iPaaS platform does not touch, running on a pattern that transfers: record-driven configuration, event-driven push, scheduled writeback, idempotency, and error isolation. Every one of those is reusable on the next flow. Retiring a middleware platform is not one project, it is a sequence of them, and this was the first.

What this adds up to

Strip away the field mappings and the integration comes down to four properties. It is configurable: credentials, endpoints, behavior flags, customer scope and location scope are all data an admin can change. It is multi-tenant: one codebase serves multiple storefronts with separate ShipStation accounts. It is idempotent: duplicates cannot be created by retries, re-saves or the writeback loop. And it is non-blocking: no failure mode stops the warehouse.

Production integrations are not defined by how they behave when everything works. They are defined by what happens when an API fails, a user retries a request, a carrier returns something unexpected, or a business rule changes. That is what this integration was designed for.

Whether native SuiteScript or an iPaaS platform is the right call for a given business is a separate question, and it does not always land the same way. We set out the full comparison, including where the platforms win, on our custom NetSuite integration page.

Paying for middleware you would rather not run?

Moving off an iPaaS platform is a sequence of flows, not a single project, and the first one sets the pattern. Whether you are weighing an off-the-shelf connector, a platform such as Boomi or Celigo, or native SuiteScript, send us your requirements and we will tell you which flows are worth moving and in what order, even when the answer is to leave things where they are. We work on fixed-price engagements, with no hourly billing and no forced sign-off. Tell us your setup.

Every behavior described in this post is implemented in the integration's three scripts: a User Event on the item fulfillment, a scheduled Map/Reduce for tracking retrieval, and a shared manager module that owns configuration and the ShipStation client. Field names shown are from the production implementation.