Skip to main content

Pattern 4: Expose

NetSuite is PASSIVE. External systems query NetSuite for data.


How It Works

EXPOSE PATTERN
─────────────────────────────────────────────────────────────────

EXTERNAL SYSTEM NETSUITE
┌─────────────────┐ ┌─────────────────┐
│ │ HTTP GET │ │
│ Query Data │ ─────────────▶ │ RESTlet or │
│ (BI Tool, App) │ │ SuiteTalk API │
│ │ ◀───────────── │ │
│ Receive Data │ JSON │ Return Data │
└─────────────────┘ └─────────────────┘

Endpoint Options:
├── RESTlet: Custom logic, filtered data
├── SuiteTalk REST: Standard CRUD, SuiteQL queries
└── SuiteTalk SOAP: Legacy, full record access

API Options Comparison

FeatureRESTletSuiteTalk RESTSuiteTalk SOAP
Custom LogicYesNoNo
Query LanguageAnySuiteQLSearch API
AuthOAuth 1.0/2.0OAuth 2.0TBA
Best ForCustom APIsStandard queriesLegacy systems
SetupScript requiredJust authenticateJust authenticate

When to Use

ScenarioCallerAPI TypeExample
BI dashboard queriesTableauSuiteTalk REST + SuiteQLSales analytics
Mobile app inventory checkCustom appRESTletStock lookup
Partner order lookupPartner systemRESTletOrder status
Website product availabilityE-commerceRESTletReal-time stock
EDI invoice pullEDI providerSuiteTalk SOAPInvoice export

Scenario A: Custom RESTlet for Mobile App

Case: Mobile app needs to check inventory levels by location.

FLOW
─────────────────────────────────────────────────────────────────

Mobile App

│ GET /restlet?sku=WIDGET-001

┌─────────────────┐
│ RESTlet │
│ (Auth required) │
└────────┬────────┘

│ Search inventory

┌─────────────────┐
│ Return JSON: │
│ { │
│ sku: "...", │
│ qty: 100, │
│ locations: [] │
│ } │
└─────────────────┘

Script: RESTlet

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @description Inventory lookup for mobile app
*/
define(['N/search', 'N/log'], function(search, log) {

/**
* GET: Lookup inventory by SKU or location
*/
function get(requestParams) {
var sku = requestParams.sku;
var location = requestParams.location;

if (!sku) {
return { error: 'SKU parameter required' };
}

var filters = [
['itemid', 'is', sku],
'AND',
['isinactive', 'is', 'F']
];

if (location) {
filters.push('AND', ['inventorylocation', 'is', location]);
}

var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: filters,
columns: [
'itemid',
'displayname',
'quantityavailable',
'quantityonhand',
'quantityonorder',
'reorderpoint',
'inventorylocation'
]
}).run().getRange({ start: 0, end: 100 });

var inventory = results.map(function(result) {
return {
sku: result.getValue('itemid'),
name: result.getValue('displayname'),
available: parseInt(result.getValue('quantityavailable')) || 0,
onHand: parseInt(result.getValue('quantityonhand')) || 0,
onOrder: parseInt(result.getValue('quantityonorder')) || 0,
reorderPoint: parseInt(result.getValue('reorderpoint')) || 0,
location: result.getText('inventorylocation')
};
});

return {
success: true,
count: inventory.length,
items: inventory
};
}

return { get: get };
});

Calling from Mobile App:

// React Native / JavaScript example
const response = await fetch(
'https://ACCOUNT.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1&sku=WIDGET-001',
{
method: 'GET',
headers: {
'Authorization': 'OAuth ...',
'Content-Type': 'application/json'
}
}
);
const data = await response.json();
// { success: true, count: 3, items: [...] }

Scenario B: Customer Portal API

Case: Customer portal needs to fetch order history and status.

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @description Customer portal API - order history
*/
define(['N/search', 'N/log'], function(search, log) {

function get(requestParams) {
var customerId = requestParams.customerId;
var status = requestParams.status; // optional filter
var limit = parseInt(requestParams.limit) || 50;

if (!customerId) {
return { error: 'customerId required' };
}

var filters = [
['entity', 'is', customerId],
'AND',
['mainline', 'is', 'T']
];

if (status) {
filters.push('AND', ['status', 'is', status]);
}

var orders = [];

search.create({
type: search.Type.SALES_ORDER,
filters: filters,
columns: [
search.createColumn({ name: 'tranid' }),
search.createColumn({ name: 'trandate', sort: search.Sort.DESC }),
search.createColumn({ name: 'status' }),
search.createColumn({ name: 'total' }),
search.createColumn({ name: 'shipaddressee' }),
search.createColumn({ name: 'shipaddress' })
]
}).run().each(function(result) {
orders.push({
orderNumber: result.getValue('tranid'),
date: result.getValue('trandate'),
status: result.getText('status'),
total: parseFloat(result.getValue('total')),
shipTo: {
name: result.getValue('shipaddressee'),
address: result.getValue('shipaddress')
}
});
return orders.length < limit;
});

return {
customerId: customerId,
orderCount: orders.length,
orders: orders
};
}

return { get: get };
});

Scenario C: SuiteTalk REST API with SuiteQL

Case: BI tool queries NetSuite for sales data using SuiteQL.

No script needed - use SuiteTalk REST API directly.

# External system queries NetSuite via SuiteQL
curl -X POST \
'https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql' \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Prefer: transient' \
-d '{
"q": "SELECT t.tranid, t.trandate, c.companyname, t.total FROM transaction t JOIN customer c ON t.entity = c.id WHERE t.type = '\''CustInvc'\'' AND t.trandate >= '\''2024-01-01'\''"
}'

Response:

{
"items": [
{
"tranid": "INV-001",
"trandate": "2024-01-15",
"companyname": "Acme Corp",
"total": 1500.00
}
],
"hasMore": false,
"totalResults": 1
}

Scenario D: Shipping Rate Calculator

Case: E-commerce site needs to calculate shipping rates based on items and destination.

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @description Calculate shipping rates for e-commerce
*/
define(['N/search', 'N/log'], function(search, log) {

function post(requestBody) {
var items = requestBody.items; // [{ sku, quantity }]
var destination = requestBody.destination; // { zip, country }

if (!items || items.length === 0) {
return { error: 'Items required' };
}

// Calculate total weight
var totalWeight = 0;
items.forEach(function(item) {
var weight = getItemWeight(item.sku);
totalWeight += weight * item.quantity;
});

// Get shipping rates based on weight and destination
var rates = calculateRates(totalWeight, destination);

return {
totalWeight: totalWeight,
destination: destination,
rates: rates
};
}

function getItemWeight(sku) {
var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: [['itemid', 'is', sku]],
columns: ['weight']
}).run().getRange({ start: 0, end: 1 });

return results.length > 0 ? parseFloat(results[0].getValue('weight')) || 0 : 0;
}

function calculateRates(weight, destination) {
// Simplified rate calculation
var baseRate = weight * 0.5; // $0.50 per lb

return [
{ method: 'Standard', days: '5-7', price: baseRate },
{ method: 'Express', days: '2-3', price: baseRate * 2 },
{ method: 'Overnight', days: '1', price: baseRate * 4 }
];
}

return { post: post };
});

Pagination for Large Results

When exposing large datasets, implement pagination:

function get(requestParams) {
var page = parseInt(requestParams.page) || 1;
var pageSize = parseInt(requestParams.pageSize) || 50;
var start = (page - 1) * pageSize;

var searchObj = search.create({
type: search.Type.CUSTOMER,
columns: ['entityid', 'email', 'companyname']
});

var pagedResults = searchObj.runPaged({ pageSize: pageSize });
var totalPages = pagedResults.pageRanges.length;

var results = [];
if (page <= totalPages) {
var pageData = pagedResults.fetch({ index: page - 1 });
pageData.data.forEach(function(result) {
results.push({
id: result.id,
name: result.getValue('companyname'),
email: result.getValue('email')
});
});
}

return {
page: page,
pageSize: pageSize,
totalPages: totalPages,
totalCount: pagedResults.count,
results: results,
hasMore: page < totalPages
};
}

Caching Frequently Accessed Data

For data that doesn't change often, use N/cache:

/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/search', 'N/cache', 'N/log'], function(search, cache, log) {

var CACHE_NAME = 'PRODUCT_CACHE';
var CACHE_TTL = 300; // 5 minutes

function get(requestParams) {
var sku = requestParams.sku;

// Try cache first
var productCache = cache.getCache({ name: CACHE_NAME });
var cached = productCache.get({ key: sku });

if (cached) {
log.debug('Cache Hit', sku);
return JSON.parse(cached);
}

// Cache miss - fetch from database
log.debug('Cache Miss', sku);
var product = fetchProduct(sku);

// Store in cache
productCache.put({
key: sku,
value: JSON.stringify(product),
ttl: CACHE_TTL
});

return product;
}

function fetchProduct(sku) {
// Fetch from database
var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: [['itemid', 'is', sku]],
columns: ['displayname', 'salesprice', 'quantityavailable']
}).run().getRange({ start: 0, end: 1 });

if (results.length === 0) {
return { error: 'Product not found' };
}

return {
sku: sku,
name: results[0].getValue('displayname'),
price: results[0].getValue('salesprice'),
available: results[0].getValue('quantityavailable')
};
}

return { get: get };
});

API Design Best Practices

PracticeExample
Use consistent response format{ success: true, data: {...} }
Return appropriate HTTP codes200 OK, 400 Bad Request, 404 Not Found
Include pagination for lists{ page, pageSize, totalPages, results }
Version your API/v1/products, /v2/products
Log all requestsFor debugging and audit
Validate input parametersCheck required fields
Document your endpointsCreate API documentation

  • Receive - Accept data pushed by external systems
  • Pull In - Fetch data from external systems