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
| Feature | RESTlet | SuiteTalk REST | SuiteTalk SOAP |
|---|---|---|---|
| Custom Logic | Yes | No | No |
| Query Language | Any | SuiteQL | Search API |
| Auth | OAuth 1.0/2.0 | OAuth 2.0 | TBA |
| Best For | Custom APIs | Standard queries | Legacy systems |
| Setup | Script required | Just authenticate | Just authenticate |
When to Use
| Scenario | Caller | API Type | Example |
|---|---|---|---|
| BI dashboard queries | Tableau | SuiteTalk REST + SuiteQL | Sales analytics |
| Mobile app inventory check | Custom app | RESTlet | Stock lookup |
| Partner order lookup | Partner system | RESTlet | Order status |
| Website product availability | E-commerce | RESTlet | Real-time stock |
| EDI invoice pull | EDI provider | SuiteTalk SOAP | Invoice 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
| Practice | Example |
|---|---|
| Use consistent response format | { success: true, data: {...} } |
| Return appropriate HTTP codes | 200 OK, 400 Bad Request, 404 Not Found |
| Include pagination for lists | { page, pageSize, totalPages, results } |
| Version your API | /v1/products, /v2/products |
| Log all requests | For debugging and audit |
| Validate input parameters | Check required fields |
| Document your endpoints | Create API documentation |