Skip to main content

Generated page. Written from IdaMilk/SunApps_NetSuite at commit 44cfdc8 on 2026-07-28, version 1.2.8. The repository has no README, so everything below is inferred from source.

SunApps NetSuite Service

What is it?

The single service that talks to NetSuite. Every SunApps request bound for the ERP passes through here, where it is signed with OAuth 1.0a, rate limited, attributed to a user, and forwarded to the NetSuite REST API.

Its own package.json describes it as the "OAuth gateway for all NetSuite communication", which is exactly right.

Purpose

NetSuite authentication is awkward: OAuth 1.0a token-based auth with a consumer key/secret pair, a token id/secret pair, and a realm. Doing that correctly in every service that needs NetSuite would mean distributing those credentials widely and re-implementing signing repeatedly.

Concentrating it here means the NetSuite credentials live in exactly one place, the rate limit is enforced globally rather than per-caller, and every write can be consistently attributed to the user who triggered it.

How it Works

Two kinds of route sit in front of the same signing layer.

Generic passthrough. Wildcard handlers for GET, POST, PUT, PATCH and DELETE forward any path straight to the NetSuite REST API. A caller can reach any NetSuite record type without this service needing to know about it. stripNsRestBaseUrl() normalises incoming URLs, so callers may pass either a bare path or a full NetSuite REST URL.

Named operations. Specific endpoints wrap multi-step work that would be clumsy to express as raw REST — creating an invoice, receiving items, triggering an inventory count, unshipping an inventory assignment, attaching files, reprinting pallet labels.

Requests are queued through a Bottleneck limiter set to 6 concurrent and a 200 ms minimum spacing — roughly 5 requests per second. NetSuite enforces concurrency limits per account, and exceeding them produces failures that look like random errors; the limiter is what keeps that from happening.

The service also stamps who did it. getLastModifiedByField() maps the target endpoint to the right custom field, and the caller's identity — passed as an x-netsuite-user header — is written into it. NetSuite's own audit trail would otherwise attribute every change to the single integration user.

Record"Last modified by" field
Transactions (work order, item receipt, item fulfilment, transform)custbody_tran_last_modified_by
customrecord_palletcustrecord_pallet_last_modified_by
customrecord_pallet_detailscustrecord_pd_last_modified_by
customrecord_samplecustrecord_sample_last_modified_by
customrecord_qc_inspectioncustrecord_qci_last_modified_by

API

MethodPathPurpose
GET/Health check
POST/api/netsuite/restletCall an arbitrary RESTlet
POST/api/netsuite/create-invoiceCreate a fulfilment invoice from a sales order
POST/api/netsuite/create-item-receiptCreate an item receipt
POST/api/netsuite/item-fulfillmentCreate an item fulfilment
POST/api/netsuite/count-inventoryTrigger an inventory count
GET/api/netsuite/inventory-progress/:taskIdPoll a count's Map/Reduce task status
POST/api/netsuite/unship-inventory-assignmentReverse an inventory assignment
POST/api/netsuite/trigger-reprintReprint a pallet label via Suitelet
POST/api/netsuite/attach-file-to-recordAttach a file to a record
POST/api/netsuite/upload-netsuite-fileUpload to the File Cabinet
POST/api/netsuite/pallet-details-exportExport pallet details
any/api/netsuite/*Generic signed passthrough

The wildcard routes are registered after the named ones, so a specific handler always wins. Adding a named route that collides with an existing path would silently change behaviour for existing callers.

Interfaces

SystemDirectionPurpose
SunApps GatewayinboundProxies /api/netsuite here
SunApps MESinboundPosts ERP transactions via the Core API
NetSuite REST APIoutboundOAuth 1.0a signed requests
SuiteScriptsoutboundCalls RESTlets and Suitelets by script ID
Gateway /internal/notifyoutboundSends the "invoice created" notification email

The link back to SuiteScripts is direct: NETSUITE_CREATE_INVOICE_SCRIPT_ID defaults to customscript_create_invoice_from_so, which is the deployment of createInvoiceFromSO.js in that repository.

Tech Stack

TechnologyVersionPurpose
Express5.2.1HTTP framework
oauth-1.0a2.2.6NetSuite request signing
bottleneck2.19.5Rate limiting (6 concurrent, 200 ms spacing)
morgan1.10.1Request logging
rotating-file-stream3.2.9Log rotation
cors2.8.6Cross-origin support
dotenv17.4.2Configuration

No database — this service holds no state of its own.

Operations

Deployed by GitHub Actions to an internal application server under a process supervisor, using the same development / production / tagging workflow trio as the rest of the platform.

Configuration, names only:

GroupVariables
ServerPORT (default 4001)
NetSuite authNETSUITE_ACCOUNT_ID, NETSUITE_REALM, NETSUITE_CONSUMER_KEY, NETSUITE_CONSUMER_SECRET, NETSUITE_TOKEN_ID, NETSUITE_TOKEN_SECRET
Script referencesNETSUITE_CREATE_INVOICE_SCRIPT_ID, NETSUITE_CREATE_INVOICE_DEPLOY_ID, NETSUITE_PALLET_REPRINT_SUITLET_ID, NETSUITE_UNSHIP_INVENTORY_ASSIGNMENT_SCRIPT_ID
NotificationCORE_API_URL, INVOICE_NOTIFY_EMAILS

Two behaviours in the config are easy to trip over:

  • INVOICE_NOTIFY_EMAILS uses ??, not ||. Setting it to an empty string suppresses the invoice email; only leaving it unset falls back to the built-in recipient list. That distinction is deliberate — it is how the catch-up script runs without emailing anyone — and it is invisible unless you read the config.
  • Several script and deployment IDs have hardcoded defaults. If a script is redeployed in NetSuite under a new ID and the environment variable is not set, the service keeps calling the old default.

scripts/ holds operational one-offs run by hand: a bin migration Map/Reduce, an invoice catch-up, and an unbilled-tail export.

Repository

RepositoryIdaMilk/SunApps_NetSuite
Version1.2.8
Primary languageJavaScript (Node, CommonJS)
Files18
Last activity2026-07-27
StatusActive
READMENone — the highest-value documentation gap in the platform, given every ERP write passes through here