NetSuite RESTlet Capabilities

Type: ERP Integration System: Oracle NetSuite (SuiteScript 2.x) Confidence: 0.91 Sources: 8 Verified: 2026-03-01 Freshness: 2026-03-01

TL;DR

System Profile

NetSuite RESTlets are server-side SuiteScript scripts that expose custom RESTful endpoints within Oracle NetSuite. They support GET, POST, PUT, and DELETE HTTP methods, and can return JSON or plain text responses. RESTlets operate within SuiteScript 2.x and have complete access to all NetSuite APIs including N/record, N/search, N/query, N/http, and N/file modules. This card covers all NetSuite editions that include SuiteCloud licensing (required for custom scripts). [src2]

RESTlets are distinct from NetSuite's built-in REST Web Services (record-based CRUD via SuiteTalk REST) and from Suitelets (which can serve HTML pages to browsers). RESTlets are purpose-built for machine-to-machine integration where custom server-side logic is required. [src6]

PropertyValue
VendorOracle
SystemOracle NetSuite (SuiteScript 2.x)
API SurfaceRESTlet (custom REST endpoints via SuiteScript)
Current API VersionSuiteScript 2.x (2025.1+)
Editions CoveredAll editions with SuiteCloud license (Standard, Mid-Market, Enterprise)
DeploymentCloud
API DocsSuiteScript 2.x RESTlet Reference
StatusGA (Generally Available)

API Surfaces & Capabilities

RESTlets are one of several integration surfaces available in NetSuite. This table compares all major options so agents can route users to the correct card. [src6]

API SurfaceProtocolBest ForMax Records/RequestGovernance LimitReal-time?Custom Logic?
RESTletHTTPS/JSON or textCustom integration logic, complex multi-step operationsUnlimited (governance-limited)5,000 units/executionYesFull SuiteScript access
REST Web ServicesHTTPS/JSONStandard record CRUD, no custom code needed1,000 records/pageAccount-level concurrencyYesNo (record-based only)
SuiteTalk SOAPHTTPS/XMLLegacy integrations, metadata operations1,000 records/pageAccount-level concurrencyYesNo (record-based only)
SuiteletHTTPS/HTML or JSONCustom UI pages, portals, internal toolsUnlimited (governance-limited)1,000 units/executionYesFull SuiteScript access
Scheduled ScriptN/A (cron-based)Batch processing, nightly ETLUnlimited (governance-limited)10,000 units/executionNo (scheduled)Full SuiteScript access
Map/ReduceN/A (async)High-volume data processing, parallelizedUnlimited (governance-limited)10,000 units/stageNo (async)Full SuiteScript access
CSV ImportHTTPS/CSVBulk data loading, manual or scripted imports25,000 rows per fileN/ANo (batch)Limited (transform scripts)

Rate Limits & Quotas

Per-Request Limits

Limit TypeValueApplies ToNotes
Governance units per execution5,000Each RESTlet invocation5x more than Suitelets (1,000), same as User Event (all phases combined) [src1]
Max input payload size10 MBRequest body (JSON or text)Guaranteed minimum; larger payloads may occasionally succeed but not reliably [src1]
Max output payload size10 MBResponse body (JSON or text)Same 10 MB guarantee applies to output [src1]
Max URL length~2,048 charsGET requestsURL parameters for GET requests are size-limited

Account-Level Concurrency Limits

Service TierBase Concurrency+ Per SuiteCloud Plus LicenseExample (3 SC+ licenses)
Standard5+10 each35
Premium10+10 each40
Enterprise20+10 each50
Ultimate40+10 each70

Formula: Total Concurrency = Base Tier Limit + (SuiteCloud Plus Licenses x 10). This pool is shared across ALL web services + RESTlets since 2017.2. [src4, src7]

Limit TypeValueWindowNotes
Default concurrency per integration record5 threadsConcurrentConfigurable per integration record; max is (account limit - 1) [src4]
Per-user RESTlet concurrency5 concurrent executionsConcurrentIndividual users face a ceiling of 5 concurrent RESTlet executions [src7]
SuiteCloud Plus license+10 concurrent threads eachConcurrentPurchased add-on; does NOT always auto-provision to sandbox [src4]

Governance Unit Costs (Key Operations)

NetSuite tracks governance usage per SuiteScript API call within each RESTlet execution. Exceeding 5,000 units terminates the script immediately. [src1]

OperationGovernance CostNotes
record.load() -- Transaction record10 unitsSales orders, invoices, purchase orders
record.load() -- Non-transaction record5 unitsCustomers, items, vendors
record.load() -- Custom record2 unitsCustom record types
record.submit() -- Transaction20 unitsCreates/updates transaction records
record.submit() -- Non-transaction10 unitsCreates/updates standard records
record.submit() -- Custom record4 unitsCreates/updates custom records
search.create() + run10 unitsSaved search execution
search.lookupFields()1 unitLightweight alternative to record.load()
http.request() (external callout)10 unitsExternal HTTP calls from within RESTlet
email.send()10 unitsSending emails from RESTlet
file.load()10 unitsLoading files from File Cabinet

Check remaining governance at runtime with script.getRemainingUsage(). [src1]

Authentication

RESTlets always require authentication for external calls. Internal calls (from other SuiteScript) are authenticated automatically. [src2]

FlowUse WhenToken LifetimeRefresh?Notes
Token-Based Authentication (TBA)Server-to-server integrations, most commonDoes not expire (until revoked)N/A -- tokens are permanentUses OAuth 1.0 signature; requires integration record + token pair [src2]
OAuth 2.0Modern integrations, recommended for new devAccess token: configurable (default 60 min)Yes -- refresh token flowRequires connected app setup; preferred over TBA for new integrations [src2]
User Credentials (NLAuth)Legacy/testing onlySession-basedNoDeprecated -- do NOT use in production; no MFA support

Authentication Gotchas

Constraints

Integration Pattern Decision Tree

START -- User needs to integrate with NetSuite via custom REST endpoints
|-- What's the integration pattern?
|   |-- Real-time (individual records, <1s latency)
|   |   |-- Needs custom business logic?
|   |   |   |-- YES --> RESTlet (this card)
|   |   |   |-- NO --> REST Web Services (standard CRUD)
|   |   |-- Data volume per call?
|   |   |   |-- < 50 records --> Single RESTlet call
|   |   |   |-- 50-500 records --> RESTlet with batched operations (watch governance)
|   |   |   |-- > 500 records --> Split into multiple RESTlet calls or use Map/Reduce
|   |   |-- Need to combine multiple operations atomically?
|   |       |-- YES --> RESTlet (can wrap in single transaction)
|   |       |-- NO --> REST Web Services (simpler, no code to maintain)
|   |-- Batch/Bulk (scheduled, high volume)
|   |   |-- < 500 records per batch?
|   |   |   |-- YES --> RESTlet called on schedule (external scheduler)
|   |   |   |-- NO --> Scheduled Script or Map/Reduce (10,000 units)
|   |   |-- Need external trigger?
|   |       |-- YES --> RESTlet (external system calls on schedule)
|   |       |-- NO --> Scheduled Script (internal NetSuite scheduler)
|   |-- Event-driven
|   |   |-- External system needs to push to NetSuite?
|   |   |   |-- YES --> RESTlet as webhook receiver
|   |   |   |-- NO --> User Event Script (internal triggers)
|   |   |-- NetSuite needs to push to external system?
|   |       |-- YES --> User Event Script + N/http callout
|   |       |-- NO --> N/A
|   |-- File-based (CSV/XML)
|       |-- Use CSV Import or SuiteTalk SOAP (not RESTlet)
|-- Which direction?
|   |-- Inbound (writing to NetSuite) --> RESTlet POST/PUT entry points
|   |-- Outbound (reading from NetSuite) --> RESTlet GET entry point
|   |-- Bidirectional --> Design conflict resolution strategy FIRST
|-- Error tolerance?
    |-- Zero-loss required --> Implement idempotency keys + retry logic in caller
    |-- Best-effort --> Fire-and-forget with exponential backoff retry

Quick Reference

RESTlet Entry Points (SuiteScript 2.x)

Entry PointHTTP MethodParameter TypeTypical UseNotes
get(requestParams)GETURL query parameters (parsed as object)Retrieve records, run searchesParameters auto-parsed to key-value object [src2]
post(requestBody)POSTJSON request body (parsed as object)Create records, execute operationsBody must be valid JSON [src2]
put(requestBody)PUTJSON request body (parsed as object)Update/upsert recordsBody must be valid JSON [src2]
delete(requestParams)DELETEURL query parameters (parsed as object)Delete recordsParameters auto-parsed to key-value object [src2]

RESTlet URL Format

https://{accountId}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script={scriptId}&deploy={deployId}

The URL is provided on the Script Deployment record after deployment. [src5]

Step-by-Step Integration Guide

1. Create the RESTlet Script File

Upload a SuiteScript 2.x file to the File Cabinet. The script exports entry point functions for each HTTP method. [src2, src5]

/**
 * @NApiVersion 2.x
 * @NScriptType Restlet
 * @NModuleScope SameAccount
 */
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
    function get(requestParams) {
        log.debug('GET params', JSON.stringify(requestParams));
        var customerId = requestParams.customerId;
        if (!customerId) throw new Error('Missing required parameter: customerId');
        var rec = record.load({ type: record.Type.CUSTOMER, id: customerId });
        return {
            id: rec.id,
            companyName: rec.getValue('companyname'),
            email: rec.getValue('email'),
            phone: rec.getValue('phone')
        };
    }
    function post(requestBody) {
        log.debug('POST body', JSON.stringify(requestBody));
        var rec = record.create({ type: record.Type.CUSTOMER });
        rec.setValue('companyname', requestBody.companyName);
        rec.setValue('email', requestBody.email);
        var id = rec.save();
        return { success: true, id: id };
    }
    return { get: get, post: post };
});

Verify: Upload file to File Cabinet > SuiteScripts folder. Confirm file appears in File Cabinet listing.

2. Create Script Record and Deployment

Navigate to Customization > Scripting > Scripts > New, select the uploaded file, save the Script Record, then create a Deployment with Status = Released. [src5]

Script Record:
  Name:        "Customer RESTlet"
  Script File:  SuiteScripts/customer_restlet.js
  Script ID:    _customer_restlet

Deployment Record:
  Title:        "Customer RESTlet Deployment"
  Deployed:     Yes
  Status:       Released    <-- MUST be Released for external access
  Execute As:   <Integration Role>
  Log Level:    Debug (development) / Error (production)

Verify: After saving the deployment, the External URL field shows the endpoint URL.

3. Set Up Authentication (TBA)

Create an Integration Record and generate token credentials. [src2]

1. Setup > Integration > Manage Integrations > New
   - Name: "My Integration"
   - State: Enabled
   - Token-Based Authentication: checked
   -> Save. Copy Consumer Key + Consumer Secret (shown only once).

2. Setup > Integration > Manage Tokens > New Access Token
   - Application: "My Integration"
   - User: <integration user>
   - Role: <integration role>
   -> Save. Copy Token ID + Token Secret (shown only once).

Verify: Attempt an authenticated GET request (see cURL example below).

4. Call the RESTlet Externally

Use the OAuth 1.0 (TBA) signed request to invoke the RESTlet. [src2]

curl -X GET \
  "https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=SCRIPT_ID&deploy=DEPLOY_ID&customerId=12345" \
  -H "Authorization: OAuth realm=\"ACCOUNT_ID\",oauth_consumer_key=\"CK\",oauth_token=\"TK\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"TS\",oauth_nonce=\"NONCE\",oauth_version=\"1.0\",oauth_signature=\"SIG\"" \
  -H "Content-Type: application/json"

Verify: Response is 200 OK with JSON body containing customer data.

Code Examples

Python: Call a NetSuite RESTlet with TBA Authentication

# Input:  NetSuite account ID, consumer/token credentials, RESTlet script/deploy IDs
# Output: JSON response from RESTlet

import requests
from requests_oauthlib import OAuth1

# Pin: requests==2.31.0, requests-oauthlib==1.3.1
auth = OAuth1(
    client_key="CONSUMER_KEY",
    client_secret="CONSUMER_SECRET",
    resource_owner_key="TOKEN_ID",
    resource_owner_secret="TOKEN_SECRET",
    realm="ACCOUNT_ID",
    signature_method="HMAC-SHA256",
)

url = "https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1"
response = requests.get(url + "&customerId=12345", auth=auth,
                        headers={"Content-Type": "application/json"}, timeout=30)
print(response.json())

JavaScript/Node.js: Call a NetSuite RESTlet with OAuth 1.0

// Input:  NetSuite credentials, RESTlet URL
// Output: JSON response from RESTlet
// Pin: [email protected]
const OAuth = require('oauth-1.0a');
const crypto = require('crypto');

const oauth = OAuth({
  consumer: { key: 'CONSUMER_KEY', secret: 'CONSUMER_SECRET' },
  signature_method: 'HMAC-SHA256',
  hash_function(base_string, key) {
    return crypto.createHmac('sha256', key).update(base_string).digest('base64');
  },
});
const token = { key: 'TOKEN_ID', secret: 'TOKEN_SECRET' };
const url = 'https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1&customerId=12345';
const authHeader = oauth.toHeader(oauth.authorize({ url, method: 'GET' }, token));
authHeader.Authorization += ', realm="ACCOUNT_ID"';

fetch(url, { headers: { ...authHeader, 'Content-Type': 'application/json' } })
  .then(r => r.json()).then(console.log);

cURL: Quick RESTlet test (OAuth 2.0)

# Input:  OAuth 2.0 client credentials
# Output: RESTlet JSON response

# Step 1: Obtain access token
ACCESS_TOKEN=$(curl -s -X POST \
  "https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token" \
  -d "grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET" \
  | jq -r '.access_token')

# Step 2: Call RESTlet with Bearer token
curl -X GET \
  "https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1&customerId=12345" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json"

Data Mapping

RESTlet Input/Output Data Types

SuiteScript TypeJSON RepresentationInput ParsingOutput SerializationGotcha
String"value"DirectDirectNetSuite internally trims whitespace on some fields
Integer (internal ID)12345Auto-parsed from JSONReturns as numberMust be numeric -- string IDs cause silent failures on record.load()
Booleantrue/falseNetSuite uses "T"/"F" internallyRESTlet can return native booleansgetValue() returns "T" not true
Date"3/1/2026"Must match user date format preferenceReturns as formatted stringDate format depends on execution user's preferences -- not ISO 8601
Currency99.99Auto-parsedReturns as numberPrecision depends on currency setup; multi-currency needs explicit conversion
Multi-select[1, 2, 3]Array of internal IDsReturns arrayMust use setValue with array; individual calls overwrite
Sublist line itemsNested arrayMust iterate with setSublistValue()Must iterate with getSublistValue()No bulk sublist set -- each line costs governance units

Data Type Gotchas

Error Handling & Failure Points

Common Error Codes

HTTP CodeSuiteScript ErrorMeaningCauseResolution
400UNEXPECTED_ERRORUnhandled runtime errorScript threw uncaught exceptionWrap in try/catch; return structured error in body [src3]
400SSS_INVALID_SCRIPTLET_IDRESTlet not foundWrong script/deploy ID, or deployment not ReleasedVerify script ID, deploy ID, status = Released [src3]
401INVALID_LOGIN_ATTEMPTAuthentication failedBad OAuth signature, expired token, wrong account IDRegenerate OAuth signature; verify credentials [src3]
403FORBIDDENWrong domainRESTlet URL pointed at wrong data centerUse dynamic URL from deployment record [src3]
405METHOD_NOT_ALLOWEDUnsupported HTTP methodRESTlet doesn't export entry point for method usedAdd the missing entry point function [src3]
415UNSUPPORTED_MEDIA_TYPEWrong Content-TypeContent-Type not application/json or text/plainSet Content-Type: application/json [src3]
302MOVED_TEMPORARILYWrong data centerRequest routed to incorrect NetSuite data centerRegenerate URL; follow redirect with re-signed request [src3]
503SERVICE_UNAVAILABLEDatabase offlineMaintenance window or connectivity issueRetry with backoff; check status.netsuite.com [src3]

Failure Points in Production

Anti-Patterns

Wrong: Returning HTTP status codes for error semantics

// BAD -- you CANNOT control HTTP status codes in RESTlets
function post(requestBody) {
    var rec = record.load({ type: 'customer', id: requestBody.id });
    if (!rec) {
        return { status: 404, message: 'Customer not found' }; // Still returns HTTP 200!
    }
    return rec;
}

Correct: Use structured response body for error handling

// GOOD -- embed error semantics in a consistent response envelope
function post(requestBody) {
    try {
        var rec = record.load({ type: record.Type.CUSTOMER, id: requestBody.id });
        return { success: true, data: { id: rec.id, name: rec.getValue('companyname') } };
    } catch (e) {
        return { success: false, error: { code: e.name, message: e.message } };
    }
}

Wrong: Loading full records when you only need a few fields

// BAD -- record.load() costs 5-10 governance units; 500 records = 2,500-5,000 units
function get(requestParams) {
    var results = [];
    var ids = requestParams.ids.split(',');
    ids.forEach(function(id) {
        var rec = record.load({ type: record.Type.CUSTOMER, id: id }); // 5 units each
        results.push({ id: rec.id, name: rec.getValue('companyname') });
    });
    return results;
}

Correct: Use search.lookupFields or search.create for bulk reads

// GOOD -- search.lookupFields = 1 unit; search.create = 10 units total
function get(requestParams) {
    var ids = requestParams.ids.split(',');
    var s = search.create({
        type: search.Type.CUSTOMER,
        filters: [['internalid', 'anyof', ids]],
        columns: ['companyname', 'email']
    });
    var results = [];
    s.run().each(function(result) {
        results.push({ id: result.id, name: result.getValue('companyname') });
        return true;
    });
    return results; // 10 units total regardless of result count
}

Wrong: Not implementing idempotency for POST operations

// BAD -- duplicate POSTs create duplicate records
function post(requestBody) {
    var rec = record.create({ type: record.Type.SALES_ORDER });
    rec.setValue('entity', requestBody.customerId);
    var id = rec.save(); // Creates NEW record every time
    return { id: id };
}

Correct: Use external ID or idempotency key

// GOOD -- check for existing record using external ID first
function post(requestBody) {
    var externalId = requestBody.externalOrderId;
    var existing = search.lookupFields({
        type: 'salesorder', id: externalId, columns: ['internalid']
    });
    if (existing && existing.internalid) {
        return { success: true, id: existing.internalid, created: false };
    }
    var rec = record.create({ type: record.Type.SALES_ORDER });
    rec.setValue('externalid', externalId);
    rec.setValue('entity', requestBody.customerId);
    var id = rec.save();
    return { success: true, id: id, created: true };
}

Common Pitfalls

Diagnostic Commands

# Check account-level API/RESTlet usage (from within NetSuite)
# Navigate to: Setup > Integration > Web Services Usage Log
# Filters: Script Type = RESTlet, Date range, Status

# Test TBA authentication from command line
curl -v -X GET \
  "https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=ID&deploy=ID" \
  -H "Authorization: OAuth realm=\"ACCOUNT_ID\",...,oauth_signature=\"SIG\"" \
  -H "Content-Type: application/json"
# Expected: 200 OK (or 401 if credentials wrong)

# Check remaining governance (add to SuiteScript)
# log.debug('Remaining', runtime.getCurrentScript().getRemainingUsage());
# View: Customization > Scripting > Script Execution Log

# Verify deployment status
# Navigate to: Customization > Scripting > Script Deployments
# Filter: Script Type = RESTlet, Status = Released

# Monitor concurrency
# Setup > Integration > Web Services Usage Log > Concurrency tab

# Check NetSuite system status
# https://status.netsuite.com

Version History & Compatibility

SuiteScript VersionReleaseStatusBreaking ChangesMigration Notes
SuiteScript 2.12020.1CurrentES2019 syntax supported (async/await, arrow functions)Use @NApiVersion 2.1; all 2.0 code is compatible
SuiteScript 2.02015.2SupportedModule-based architecture (define/require)N/ module imports replace nlapiXxx functions
SuiteScript 1.0LegacyMaintenance-onlyDifferent entry point signatures, different governanceNot recommended; 1.0 has recovery points (2.x does not)

Account-level concurrency governance introduced in 2017.2 -- the most significant change affecting RESTlet design patterns. [src4]

Deprecation Policy

Oracle NetSuite supports SuiteScript APIs indefinitely but marks older patterns as "not recommended." SuiteScript 1.0 is still functional but receives no new features. API changes are communicated via quarterly release notes (two releases per year: .1 and .2). Breaking changes are rare and announced 2 releases in advance. [src2]

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
Custom business logic in NetSuite's contextStandard record CRUD onlyNetSuite REST Web Services
Multiple operations in single atomic call>500 records per operationScheduled Script or Map/Reduce
External system pushes data in real-timeHTML pages or forms for end usersSuitelets
Custom response formats (aggregated/computed)File-based bulk import (CSV, XML)CSV Import or SuiteTalk SOAP
Webhook receiver for external eventsLong-running operations (>30 seconds)Scheduled Script with status tracking
Expose NetSuite data to non-NetSuite appsReal-time change notifications FROM NetSuiteUser Event Scripts + N/http callout

Important Caveats

Related Units