Skip to main content

RESTlet Development

RESTlets are server-side scripts that expose custom REST endpoints for external system integration.


When to Use RESTlets

Use CaseExample
External integrationsConnect with third-party systems
Mobile appsProvide data to mobile applications
Custom APIsCreate specific data endpoints
WebhooksReceive notifications from external services
AutomationAllow external systems to trigger actions

RESTlet Execution Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ RESTLET EXECUTION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ EXTERNAL SYSTEM │
│ (Mobile App, Website, Integration Platform) │
└──────────────────────────────┬───────────────────────────────────┘

│ HTTPS Request
│ + Authentication


┌──────────────────────────────────────────────────────────────────┐
│ NETSUITE AUTHENTICATION │
│ ────────────────────────────────────────────────────────────────│
│ Token-Based Authentication (TBA) │
│ • Consumer Key/Secret │
│ • Token ID/Secret │
│ • OAuth 1.0 signature │
└──────────────────────────────┬───────────────────────────────────┘

┌─────────────────────┼─────────────────────┐
│ │ │
│ Auth Auth Auth
│ Failed Success Success
▼ │ │
┌─────────────────┐ │ │
│ 401 │ │ │
│ Unauthorized │ │ │
└─────────────────┘ │ │
│ │
┌────────────────────┴─────────────────────┤
│ │
▼ ▼
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ HTTP METHOD ROUTING │ │ Content-Type Header │
└─────────────────┬───────────────────┘ │ Determines data format │
│ └─────────────────────────────────────┘
┌───────────────┼───────────────┬───────────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ GET │ │ POST │ │ PUT │ │ DELETE │
│ ───────── │ │ ───────── │ │ ───────── │ │ ───────── │
│ Retrieve │ │ Create │ │ Update │ │ Remove │
│ data │ │ data │ │ data │ │ data │
│ │ │ │ │ │ │ │
│ get() │ │ post() │ │ put() │ │ delete() │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │
└───────────────┴───────────────┴───────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ SCRIPT EXECUTION │
│ ────────────────────────────────────────────────────────────────│
│ • Process request data │
│ • Execute business logic │
│ • Query/Create/Update records │
│ • Return response object │
│ • Governance: 5,000 units │
└──────────────────────────────┬───────────────────────────────────┘

│ Return object/array/string


┌──────────────────────────────────────────────────────────────────┐
│ RESPONSE │
│ ────────────────────────────────────────────────────────────────│
│ • Automatic JSON/XML serialization │
│ • HTTP status codes │
│ • Response headers │
└──────────────────────────────────────────────────────────────────┘

HTTP Methods

MethodFunctionPurposeRequest Body
GETget(context)Retrieve dataQuery params only
POSTpost(context)Create dataYes
PUTput(context)Update dataYes
DELETEdelete(context)Delete dataOptional

Basic RESTlet Structure

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @NModuleScope SameAccount
*/
define(['N/record', 'N/search', 'N/log', 'N/error'],
(record, search, log, error) => {

/**
* GET - Retrieve data
* @param {Object} context - Request parameters
* @returns {Object|Array} - Response data
*/
const get = (context) => {
log.debug('GET Request', JSON.stringify(context));

// context contains query parameters
const recordId = context.id;
const recordType = context.type;

if (!recordId || !recordType) {
throw error.create({
name: 'MISSING_PARAMS',
message: 'id and type parameters are required'
});
}

// Load and return record data
const rec = record.load({
type: recordType,
id: recordId
});

return {
success: true,
data: {
id: rec.id,
type: rec.type,
name: rec.getValue({ fieldId: 'name' })
}
};
};

/**
* POST - Create data
* @param {Object} context - Request body
* @returns {Object} - Response with created ID
*/
const post = (context) => {
log.debug('POST Request', JSON.stringify(context));

// context is the parsed request body
const { type, values } = context;

if (!type || !values) {
throw error.create({
name: 'INVALID_REQUEST',
message: 'type and values are required'
});
}

// Create record
const newRecord = record.create({
type: type,
isDynamic: true
});

// Set field values
Object.keys(values).forEach(fieldId => {
newRecord.setValue({
fieldId: fieldId,
value: values[fieldId]
});
});

const recordId = newRecord.save();

return {
success: true,
id: recordId,
message: `${type} created successfully`
};
};

/**
* PUT - Update data
* @param {Object} context - Request body
* @returns {Object} - Response
*/
const put = (context) => {
log.debug('PUT Request', JSON.stringify(context));

const { type, id, values } = context;

if (!type || !id || !values) {
throw error.create({
name: 'INVALID_REQUEST',
message: 'type, id, and values are required'
});
}

// Update record
record.submitFields({
type: type,
id: id,
values: values
});

return {
success: true,
id: id,
message: `${type} updated successfully`
};
};

/**
* DELETE - Remove data
* @param {Object} context - Request parameters
* @returns {Object} - Response
*/
const doDelete = (context) => {
log.debug('DELETE Request', JSON.stringify(context));

const { type, id } = context;

if (!type || !id) {
throw error.create({
name: 'INVALID_REQUEST',
message: 'type and id are required'
});
}

// Delete record
record.delete({
type: type,
id: id
});

return {
success: true,
id: id,
message: `${type} deleted successfully`
};
};

return {
get,
post,
put,
'delete': doDelete // 'delete' is reserved word
};
});

Request/Response Flow

┌─────────────────────────────────────────────────────────────────────────────┐
│ REQUEST CONTENT TYPES │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Content-Type: application/json │
│ ────────────────────────────────────────────────────────────────│
│ Request: {"name": "Acme Corp", "email": "info@acme.com"} │
│ context = parsed JSON object │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Content-Type: text/plain │
│ ────────────────────────────────────────────────────────────────│
│ Request: Plain text content │
│ context = string │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ GET Request (Query Parameters) │
│ ────────────────────────────────────────────────────────────────│
│ URL: ?type=customer&id=123&fields=name,email │
│ context = { type: 'customer', id: '123', fields: 'name,email' } │
└──────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────┐
│ RESPONSE HANDLING │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Return Object → Automatic JSON │
│ ────────────────────────────────────────────────────────────────│
│ return { success: true, data: {...} }; │
│ Response: {"success":true,"data":{...}} │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Return Array → Automatic JSON │
│ ────────────────────────────────────────────────────────────────│
│ return [{ id: 1 }, { id: 2 }]; │
│ Response: [{"id":1},{"id":2}] │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Return String → Plain text │
│ ────────────────────────────────────────────────────────────────│
│ return "Operation successful"; │
│ Response: Operation successful │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ Throw Error → HTTP 4xx/5xx │
│ ────────────────────────────────────────────────────────────────│
│ throw error.create({ name: 'NOT_FOUND', message: '...' }); │
│ Response: 400 Bad Request with error details │
└──────────────────────────────────────────────────────────────────┘

Complete Example: Customer API

/**
* @NApiVersion 2.1
* @NScriptType Restlet
* @NModuleScope SameAccount
* @description Customer CRUD API
*/
define(['N/record', 'N/search', 'N/log', 'N/error'],
(record, search, log, error) => {

/**
* GET - Retrieve customer(s)
*/
const get = (context) => {
log.audit('Customer API', `GET: ${JSON.stringify(context)}`);

try {
// Single customer lookup
if (context.id) {
return getCustomerById(context.id);
}

// Search customers
return searchCustomers(context);

} catch (e) {
log.error('GET Error', e.message);
return createErrorResponse(e);
}
};

/**
* Get single customer by ID
*/
const getCustomerById = (customerId) => {
const customerRecord = record.load({
type: record.Type.CUSTOMER,
id: customerId
});

return {
success: true,
data: mapCustomerRecord(customerRecord)
};
};

/**
* Search customers with filters
*/
const searchCustomers = (params) => {
const filters = [['isinactive', 'is', 'F']];

// Add optional filters
if (params.email) {
filters.push('AND', ['email', 'contains', params.email]);
}
if (params.name) {
filters.push('AND', ['entityid', 'contains', params.name]);
}
if (params.subsidiary) {
filters.push('AND', ['subsidiary', 'anyof', params.subsidiary]);
}

const customerSearch = search.create({
type: search.Type.CUSTOMER,
filters: filters,
columns: [
'internalid',
'entityid',
'companyname',
'email',
'phone',
'salesrep',
'balance'
]
});

const results = [];
const limit = parseInt(params.limit) || 100;
const offset = parseInt(params.offset) || 0;

customerSearch.run().getRange({
start: offset,
end: offset + limit
}).forEach(result => {
results.push({
id: result.id,
entityId: result.getValue('entityid'),
companyName: result.getValue('companyname'),
email: result.getValue('email'),
phone: result.getValue('phone'),
salesRep: result.getText('salesrep'),
balance: result.getValue('balance')
});
});

return {
success: true,
count: results.length,
offset: offset,
limit: limit,
data: results
};
};

/**
* POST - Create customer
*/
const post = (context) => {
log.audit('Customer API', `POST: ${JSON.stringify(context)}`);

try {
// Validate required fields
validateCustomerData(context);

// Create customer record
const customer = record.create({
type: record.Type.CUSTOMER,
isDynamic: true
});

// Set fields
setCustomerFields(customer, context);

// Add addresses if provided
if (context.addresses && context.addresses.length > 0) {
addAddresses(customer, context.addresses);
}

// Add contacts if provided
if (context.contacts && context.contacts.length > 0) {
addContacts(customer, context.contacts);
}

const customerId = customer.save();

return {
success: true,
id: customerId,
message: 'Customer created successfully'
};

} catch (e) {
log.error('POST Error', e.message);
return createErrorResponse(e);
}
};

/**
* PUT - Update customer
*/
const put = (context) => {
log.audit('Customer API', `PUT: ${JSON.stringify(context)}`);

try {
if (!context.id) {
throw error.create({
name: 'MISSING_ID',
message: 'Customer ID is required for update'
});
}

const customer = record.load({
type: record.Type.CUSTOMER,
id: context.id,
isDynamic: true
});

// Update fields
setCustomerFields(customer, context);

// Update addresses if provided
if (context.addresses) {
updateAddresses(customer, context.addresses);
}

customer.save();

return {
success: true,
id: context.id,
message: 'Customer updated successfully'
};

} catch (e) {
log.error('PUT Error', e.message);
return createErrorResponse(e);
}
};

/**
* DELETE - Deactivate customer
*/
const doDelete = (context) => {
log.audit('Customer API', `DELETE: ${JSON.stringify(context)}`);

try {
if (!context.id) {
throw error.create({
name: 'MISSING_ID',
message: 'Customer ID is required for deletion'
});
}

// Soft delete - set inactive
record.submitFields({
type: record.Type.CUSTOMER,
id: context.id,
values: {
'isinactive': true
}
});

return {
success: true,
id: context.id,
message: 'Customer deactivated successfully'
};

} catch (e) {
log.error('DELETE Error', e.message);
return createErrorResponse(e);
}
};

// Helper Functions

/**
* Validate required customer fields
*/
const validateCustomerData = (data) => {
const required = ['companyName', 'email'];
const missing = required.filter(field => !data[field]);

if (missing.length > 0) {
throw error.create({
name: 'VALIDATION_ERROR',
message: `Missing required fields: ${missing.join(', ')}`
});
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw error.create({
name: 'VALIDATION_ERROR',
message: 'Invalid email format'
});
}
};

/**
* Set customer record fields
*/
const setCustomerFields = (customer, data) => {
const fieldMapping = {
'companyName': 'companyname',
'email': 'email',
'phone': 'phone',
'fax': 'fax',
'subsidiary': 'subsidiary',
'salesRep': 'salesrep',
'terms': 'terms',
'creditLimit': 'creditlimit',
'category': 'category',
'comments': 'comments'
};

Object.keys(fieldMapping).forEach(apiField => {
if (data[apiField] !== undefined) {
customer.setValue({
fieldId: fieldMapping[apiField],
value: data[apiField]
});
}
});
};

/**
* Add addresses to customer
*/
const addAddresses = (customer, addresses) => {
addresses.forEach(addr => {
customer.selectNewLine({ sublistId: 'addressbook' });

const addressSubrecord = customer.getCurrentSublistSubrecord({
sublistId: 'addressbook',
fieldId: 'addressbookaddress'
});

addressSubrecord.setValue({ fieldId: 'addr1', value: addr.address1 || '' });
addressSubrecord.setValue({ fieldId: 'addr2', value: addr.address2 || '' });
addressSubrecord.setValue({ fieldId: 'city', value: addr.city || '' });
addressSubrecord.setValue({ fieldId: 'state', value: addr.state || '' });
addressSubrecord.setValue({ fieldId: 'zip', value: addr.zip || '' });
addressSubrecord.setValue({ fieldId: 'country', value: addr.country || 'US' });

customer.setCurrentSublistValue({
sublistId: 'addressbook',
fieldId: 'label',
value: addr.label || 'Main Address'
});

if (addr.isDefaultBilling) {
customer.setCurrentSublistValue({
sublistId: 'addressbook',
fieldId: 'defaultbilling',
value: true
});
}

if (addr.isDefaultShipping) {
customer.setCurrentSublistValue({
sublistId: 'addressbook',
fieldId: 'defaultshipping',
value: true
});
}

customer.commitLine({ sublistId: 'addressbook' });
});
};

/**
* Add contacts to customer
*/
const addContacts = (customer, contacts) => {
contacts.forEach(contact => {
// Create contact record
const contactRecord = record.create({
type: record.Type.CONTACT
});

contactRecord.setValue({ fieldId: 'firstname', value: contact.firstName || '' });
contactRecord.setValue({ fieldId: 'lastname', value: contact.lastName || '' });
contactRecord.setValue({ fieldId: 'email', value: contact.email || '' });
contactRecord.setValue({ fieldId: 'phone', value: contact.phone || '' });
contactRecord.setValue({ fieldId: 'title', value: contact.title || '' });

// Link to company will be set automatically when saved
contactRecord.setValue({ fieldId: 'company', value: customer.id });

contactRecord.save();
});
};

/**
* Update existing addresses
*/
const updateAddresses = (customer, addresses) => {
// Remove existing addresses
const lineCount = customer.getLineCount({ sublistId: 'addressbook' });
for (let i = lineCount - 1; i >= 0; i--) {
customer.removeLine({ sublistId: 'addressbook', line: i });
}

// Add new addresses
addAddresses(customer, addresses);
};

/**
* Map customer record to API response
*/
const mapCustomerRecord = (customer) => {
return {
id: customer.id,
entityId: customer.getValue('entityid'),
companyName: customer.getValue('companyname'),
email: customer.getValue('email'),
phone: customer.getValue('phone'),
fax: customer.getValue('fax'),
subsidiary: customer.getText('subsidiary'),
salesRep: customer.getText('salesrep'),
terms: customer.getText('terms'),
creditLimit: customer.getValue('creditlimit'),
balance: customer.getValue('balance'),
isInactive: customer.getValue('isinactive'),
dateCreated: customer.getValue('datecreated')
};
};

/**
* Create standardized error response
*/
const createErrorResponse = (err) => {
return {
success: false,
error: {
name: err.name || 'UNKNOWN_ERROR',
message: err.message || 'An unexpected error occurred'
}
};
};

return {
get,
post,
put,
'delete': doDelete
};
});

Authentication

Token-Based Authentication (TBA)

┌─────────────────────────────────────────────────────────────────────────────┐
│ TBA AUTHENTICATION SETUP │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ 1. CREATE INTEGRATION RECORD │
│ ────────────────────────────────────────────────────────────────│
│ Setup → Integration → Manage Integrations → New │
│ ✓ Token-Based Authentication │
│ │
│ Get: Consumer Key + Consumer Secret │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ 2. CREATE ACCESS TOKEN │
│ ────────────────────────────────────────────────────────────────│
│ Setup → Users/Roles → Access Tokens → New │
│ Select: Application, User, Role │
│ │
│ Get: Token ID + Token Secret │
└──────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ 3. BUILD OAUTH SIGNATURE │
│ ────────────────────────────────────────────────────────────────│
│ Required Headers: │
│ • Authorization: OAuth realm="ACCOUNT_ID", │
│ oauth_consumer_key="...", │
│ oauth_token="...", │
│ oauth_signature_method="HMAC-SHA256", │
│ oauth_timestamp="...", │
│ oauth_nonce="...", │
│ oauth_version="1.0", │
│ oauth_signature="..." │
└──────────────────────────────────────────────────────────────────┘

RESTlet URL Format

https://{ACCOUNT_ID}.restlets.api.netsuite.com/app/site/hosting/restlet.nl
?script={SCRIPT_ID}
&deploy={DEPLOYMENT_ID}

Example:
https://1234567.restlets.api.netsuite.com/app/site/hosting/restlet.nl
?script=customscript_customer_api
&deploy=customdeploy_customer_api

Calling RESTlet from External Code

JavaScript (Node.js) Example

const crypto = require('crypto');
const OAuth = require('oauth-1.0a');

const config = {
accountId: 'YOUR_ACCOUNT_ID',
consumerKey: 'YOUR_CONSUMER_KEY',
consumerSecret: 'YOUR_CONSUMER_SECRET',
tokenId: 'YOUR_TOKEN_ID',
tokenSecret: 'YOUR_TOKEN_SECRET',
scriptId: 'customscript_customer_api',
deployId: 'customdeploy_customer_api'
};

const oauth = OAuth({
consumer: {
key: config.consumerKey,
secret: config.consumerSecret
},
signature_method: 'HMAC-SHA256',
hash_function(base_string, key) {
return crypto.createHmac('sha256', key)
.update(base_string)
.digest('base64');
}
});

const url = `https://${config.accountId}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=${config.scriptId}&deploy=${config.deployId}`;

const token = {
key: config.tokenId,
secret: config.tokenSecret
};

// GET Request
const getCustomer = async (customerId) => {
const requestData = {
url: `${url}&id=${customerId}`,
method: 'GET'
};

const headers = oauth.toHeader(oauth.authorize(requestData, token));
headers['Content-Type'] = 'application/json';

const response = await fetch(requestData.url, {
method: 'GET',
headers: headers
});

return response.json();
};

// POST Request
const createCustomer = async (customerData) => {
const requestData = {
url: url,
method: 'POST'
};

const headers = oauth.toHeader(oauth.authorize(requestData, token));
headers['Content-Type'] = 'application/json';

const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(customerData)
});

return response.json();
};

// Usage
getCustomer('123').then(console.log);

createCustomer({
companyName: 'New Company',
email: 'info@newcompany.com'
}).then(console.log);

Python Example

import requests
from requests_oauthlib import OAuth1

config = {
'account_id': 'YOUR_ACCOUNT_ID',
'consumer_key': 'YOUR_CONSUMER_KEY',
'consumer_secret': 'YOUR_CONSUMER_SECRET',
'token_id': 'YOUR_TOKEN_ID',
'token_secret': 'YOUR_TOKEN_SECRET',
'script_id': 'customscript_customer_api',
'deploy_id': 'customdeploy_customer_api'
}

url = f"https://{config['account_id']}.restlets.api.netsuite.com/app/site/hosting/restlet.nl"

auth = OAuth1(
config['consumer_key'],
config['consumer_secret'],
config['token_id'],
config['token_secret'],
signature_method='HMAC-SHA256',
realm=config['account_id']
)

# GET Request
def get_customer(customer_id):
params = {
'script': config['script_id'],
'deploy': config['deploy_id'],
'id': customer_id
}

response = requests.get(
url,
params=params,
auth=auth,
headers={'Content-Type': 'application/json'}
)

return response.json()

# POST Request
def create_customer(customer_data):
params = {
'script': config['script_id'],
'deploy': config['deploy_id']
}

response = requests.post(
url,
params=params,
auth=auth,
headers={'Content-Type': 'application/json'},
json=customer_data
)

return response.json()

# Usage
customer = get_customer('123')
print(customer)

new_customer = create_customer({
'companyName': 'New Company',
'email': 'info@newcompany.com'
})
print(new_customer)

Script Deployment XML

<?xml version="1.0" encoding="UTF-8"?>
<restlet scriptid="customscript_customer_api">
<name>Customer API</name>
<scriptfile>[/SuiteScripts/RESTlets/customer_api_rl.js]</scriptfile>
<description>REST API for customer CRUD operations</description>
<isinactive>F</isinactive>
<notifyowner>T</notifyowner>

<scriptdeployments>
<scriptdeployment scriptid="customdeploy_customer_api">
<status>RELEASED</status>
<title>Customer API v1</title>
<isdeployed>T</isdeployed>
<loglevel>DEBUG</loglevel>
<allroles>F</allroles>
<audslctrole>
<role>[Administrator]</role>
<role>[Sales Manager]</role>
</audslctrole>
</scriptdeployment>
</scriptdeployments>
</restlet>

Error Handling Best Practices

/**
* Standardized error handling for RESTlets
*/
const createResponse = (success, data, errorInfo = null) => {
const response = {
success: success,
timestamp: new Date().toISOString()
};

if (success) {
response.data = data;
} else {
response.error = {
code: errorInfo?.name || 'UNKNOWN_ERROR',
message: errorInfo?.message || 'An error occurred',
details: errorInfo?.details || null
};
}

return response;
};

const get = (context) => {
try {
// Validate request
if (!context.id) {
return createResponse(false, null, {
name: 'MISSING_PARAMETER',
message: 'id parameter is required'
});
}

// Process request
const data = getRecord(context.id);

return createResponse(true, data);

} catch (e) {
log.error('RESTlet Error', e);

// Handle specific error types
if (e.name === 'RCRD_DSNT_EXIST') {
return createResponse(false, null, {
name: 'NOT_FOUND',
message: `Record with ID ${context.id} not found`
});
}

return createResponse(false, null, {
name: e.name,
message: e.message
});
}
};

Best Practices

PracticeDescription
Validate inputCheck all required fields
Return consistent formatUse standard response structure
Log requestsAudit log all API calls
Handle errors gracefullyReturn meaningful error messages
Use HTTPS onlyNever expose over HTTP
Rate limitingImplement in calling code
Version your APIUse deployment names for versions

Security Considerations

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘

☑ Use Token-Based Authentication (never basic auth)
☑ Validate all input parameters
☑ Sanitize data before database operations
☑ Limit deployment roles to necessary users
☑ Log all API access for auditing
☑ Never expose internal IDs in error messages
☑ Implement rate limiting externally
☑ Use HTTPS exclusively
☑ Rotate tokens periodically
☑ Monitor for unusual access patterns

Next Steps