---
# === IDENTITY ===
id: business/erp-integration/netsuite-restlet-capabilities/2026
canonical_question: "What are NetSuite RESTlet capabilities - 5000 governance units, per-user concurrency, HTTP methods?"
aliases:
  - "NetSuite RESTlet governance units limit"
  - "NetSuite RESTlet vs Suitelet vs REST API"
  - "NetSuite RESTlet authentication OAuth 2.0 TBA"
  - "NetSuite SuiteScript 2.x RESTlet entry points"
  - "NetSuite RESTlet concurrency limits per user"
  - "How to build custom REST endpoints in NetSuite"
entity_type: erp_integration
domain: business > erp-integration > NetSuite RESTlet
region: global
jurisdiction: global
temporal_scope: 2025-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "SuiteScript 2.x (2025.1+)"
    edition: "All editions (SuiteCloud licensed)"
    deployment: cloud
    api_surface: RESTlet

# === VERIFICATION ===
last_verified: 2026-03-01
confidence: 0.91
version: 1.0
first_published: 2026-03-01

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "2017.2 — Account-level concurrency governance introduced"
  next_review: 2026-08-28
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "5,000 governance units per RESTlet execution (5x more than Suitelets/User Event scripts at 1,000)"
  - "10 MB maximum payload size for both input and output strings"
  - "5 concurrent RESTlet calls per user (default); account-level concurrency pool shared with SOAP web services"
  - "RESTlet concurrency governed at account level since 2017.2 — all web service + RESTlet calls share one pool"
  - "Content-Type restricted to application/json or text/plain for input; JSON or plain text for output"
  - "Cannot yield or set recovery points in SuiteScript 2.x RESTlets — governance exhaustion terminates execution"
  - "HTTP status codes not directly controllable — normal return always sends 200; thrown errors always send 400"
  - "SuiteCloud license required — not available on all NetSuite tier levels"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User needs standard CRUD on NetSuite records without custom logic"
    use_instead: "business/erp-integration/netsuite-rest-api-capabilities/2026"
  - condition: "User needs to build custom NetSuite UI pages or forms"
    use_instead: "business/erp-integration/netsuite-suitelet-capabilities/2026"
  - condition: "User needs bulk data migration or high-volume ETL"
    use_instead: "business/erp-integration/netsuite-csv-import-capabilities/2026"
  - condition: "User needs SOAP web services integration (legacy)"
    use_instead: "business/erp-integration/netsuite-suitetalk-soap-capabilities/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: integration_pattern
    question: "What integration pattern do you need?"
    type: choice
    options:
      - "real-time sync (individual records, <1s latency)"
      - "batch/bulk (scheduled, high volume)"
      - "event-driven (webhook, CDC, platform events)"
      - "file-based (CSV/XML import/export)"
  - key: data_volume
    question: "What's your daily data volume?"
    type: choice
    options:
      - "< 1,000 records/day"
      - "1,000-100,000 records/day"
      - "> 100,000 records/day"
  - key: direction
    question: "What's the data flow direction?"
    type: choice
    options:
      - "inbound (writing to NetSuite)"
      - "outbound (reading from NetSuite)"
      - "bidirectional sync"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/netsuite-restlet-capabilities/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-03-01)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/netsuite-rest-api-capabilities/2026"
      label: "NetSuite REST Web Services (standard record API)"
    - id: "business/erp-integration/netsuite-suitetalk-soap-capabilities/2026"
      label: "NetSuite SuiteTalk SOAP API"
  solves:
    - id: "business/erp-integration/netsuite-shopify-order-sync/2026"
      label: "NetSuite-Shopify order sync playbook (uses RESTlets)"
  alternative_to:
    - id: "business/erp-integration/netsuite-suitelet-capabilities/2026"
      label: "NetSuite Suitelets (custom pages, 1,000 governance units)"
  often_confused_with:
    - id: "business/erp-integration/netsuite-rest-api-capabilities/2026"
      label: "NetSuite REST Web Services (built-in REST, no custom code)"

# === SOURCES ===
sources:
  - id: src1
    title: "RESTlet Governance and Security"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4640094112.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src2
    title: "SuiteScript 2.x RESTlet Reference"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4618434109.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src3
    title: "SuiteScript 2.x RESTlet Error Handling"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4634920277.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src4
    title: "Web Services and RESTlet Concurrency Governance"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1500275531.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src5
    title: "Deploying a RESTlet"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4618456517.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src6
    title: "RESTlets vs. Other NetSuite Integration Options"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_156509465078.html
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src7
    title: "NetSuite API Governance: Concurrency & Rate Limits Explained"
    author: Houseblend
    url: https://www.houseblend.io/articles/netsuite-api-governance-guide
    type: technical_blog
    published: 2025-06-15
    reliability: moderate_high
  - id: src8
    title: "Comparing NetSuite REST API and RESTlets: Pros and Cons"
    author: Kolleno
    url: https://www.linkedin.com/pulse/comparing-netsuite-rest-api-restlets-pros-cons-kolleno-limited-yw39e
    type: technical_blog
    published: 2025-03-10
    reliability: moderate
---

# NetSuite RESTlet Capabilities

## TL;DR

- **Bottom line**: RESTlets are custom-built REST endpoints in NetSuite via SuiteScript 2.x — they give you 5,000 governance units per execution and full SuiteScript API access, making them the most flexible integration surface for complex logic. [src1]
- **Key limit**: 5,000 governance units per execution (5x Suitelets), 10 MB input/output payload, 5 concurrent calls per user (default). [src1, src4]
- **Watch out for**: HTTP status codes are not controllable — normal returns always send 200 OK, thrown errors always send 400 Bad Request. Build error semantics into the response body, not HTTP codes. [src3]
- **Best for**: Custom integrations requiring complex business logic, multi-record operations in one call, or non-standard record types that the standard REST API doesn't expose. [src6]
- **Authentication**: Token-Based Authentication (TBA / OAuth 1.0) or OAuth 2.0 for external calls; automatic for internal SuiteScript calls. [src2]

## 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]

| Property | Value |
|---|---|
| **Vendor** | Oracle |
| **System** | Oracle NetSuite (SuiteScript 2.x) |
| **API Surface** | RESTlet (custom REST endpoints via SuiteScript) |
| **Current API Version** | SuiteScript 2.x (2025.1+) |
| **Editions Covered** | All editions with SuiteCloud license (Standard, Mid-Market, Enterprise) |
| **Deployment** | Cloud |
| **API Docs URL** | [SuiteScript 2.x RESTlet Reference](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4618434109.html) |
| **Status** | GA (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 Surface | Protocol | Best For | Max Records/Request | Governance Limit | Real-time? | Custom Logic? |
|---|---|---|---|---|---|---|
| **RESTlet** | HTTPS/JSON or text | Custom integration logic, complex multi-step operations | Unlimited (governance-limited) | 5,000 units/execution | Yes | Full SuiteScript access |
| **REST Web Services** | HTTPS/JSON | Standard record CRUD, no custom code needed | 1,000 records/page | Account-level concurrency | Yes | No (record-based only) |
| **SuiteTalk SOAP** | HTTPS/XML | Legacy integrations, metadata operations | 1,000 records/page | Account-level concurrency | Yes | No (record-based only) |
| **Suitelet** | HTTPS/HTML or JSON | Custom UI pages, portals, internal tools | Unlimited (governance-limited) | 1,000 units/execution | Yes | Full SuiteScript access |
| **Scheduled Script** | N/A (cron-based) | Batch processing, nightly ETL | Unlimited (governance-limited) | 10,000 units/execution | No (scheduled) | Full SuiteScript access |
| **Map/Reduce** | N/A (async) | High-volume data processing, parallelized | Unlimited (governance-limited) | 10,000 units/stage | No (async) | Full SuiteScript access |
| **CSV Import** | HTTPS/CSV | Bulk data loading, manual or scripted imports | 25,000 rows per file | N/A | No (batch) | Limited (transform scripts) |

## Rate Limits & Quotas

### Per-Request Limits

| Limit Type | Value | Applies To | Notes |
|---|---|---|---|
| Governance units per execution | 5,000 | Each RESTlet invocation | 5x more than Suitelets (1,000), same as User Event (all phases combined) [src1] |
| Max input payload size | 10 MB | Request body (JSON or text) | Guaranteed minimum; larger payloads may occasionally succeed but are not reliable [src1] |
| Max output payload size | 10 MB | Response body (JSON or text) | Same 10 MB guarantee applies to output [src1] |
| Max URL length | ~2,048 chars | GET requests | URL parameters for GET requests are size-limited |

### Account-Level Concurrency Limits

| Service Tier | Base Concurrency | + Per SuiteCloud Plus License | Example (3 SC+ licenses) |
|---|---|---|---|
| Standard | 5 | +10 each | 35 |
| Premium | 10 | +10 each | 40 |
| Enterprise | 20 | +10 each | 50 |
| Ultimate | 40 | +10 each | 70 |

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 Type | Value | Window | Notes |
|---|---|---|---|
| Default concurrency per integration record | 5 threads | Concurrent | Configurable per integration record; max is (account limit - 1) [src4] |
| Per-user RESTlet concurrency | 5 concurrent executions | Concurrent | Individual users face a ceiling of 5 concurrent RESTlet executions [src7] |
| SuiteCloud Plus license | +10 concurrent threads each | Concurrent | Purchased 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]

| Operation | Governance Cost | Notes |
|---|---|---|
| record.load() — Transaction record | 10 units | Sales orders, invoices, purchase orders |
| record.load() — Non-transaction record | 5 units | Customers, items, vendors |
| record.load() — Custom record | 2 units | Custom record types |
| record.submit() — Transaction | 20 units | Creates/updates transaction records |
| record.submit() — Non-transaction | 10 units | Creates/updates standard records |
| record.submit() — Custom record | 4 units | Creates/updates custom records |
| search.create() + run | 10 units | Saved search execution |
| search.lookupFields() | 1 unit | Lightweight alternative to record.load() |
| http.request() (external callout) | 10 units | External HTTP calls from within RESTlet |
| email.send() | 10 units | Sending emails from RESTlet |
| file.load() | 10 units | Loading 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]

| Flow | Use When | Token Lifetime | Refresh? | Notes |
|---|---|---|---|---|
| Token-Based Authentication (TBA) | Server-to-server integrations, most common | Does not expire (until revoked) | N/A — tokens are permanent | Uses OAuth 1.0 signature; requires integration record + token pair [src2] |
| OAuth 2.0 | Modern integrations, recommended for new development | Access token: configurable (default 60 min) | Yes — refresh token flow | Requires connected app setup; preferred over TBA for new integrations [src2] |
| User Credentials (NLAuth) | Legacy/testing only | Session-based | No | Deprecated approach — do NOT use in production; no MFA support |

### Authentication Gotchas

- TBA uses OAuth 1.0 signing (not OAuth 2.0) — the consumer key, consumer secret, token ID, and token secret must all be present in the Authorization header. Signature base string must be computed correctly including the account ID. [src2]
- OAuth 2.0 for RESTlets requires a separate setup from OAuth 2.0 for REST Web Services — the integration record must have RESTlet scope enabled and a specific callback URL configured. [src2]
- The NLAuth header format uses email + password + account ID + role ID — it's the simplest to implement but provides no security for production. NetSuite may block NLAuth in future releases. [src7]
- Session timeout for token-based auth is not configurable per RESTlet — it follows the company-wide session timeout setting. [src2]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- 5,000 governance units is a hard cap — no yield, no recovery points, no "continue later" in SuiteScript 2.x. If your operation exceeds 5,000 units, you must split it across multiple RESTlet calls or use Scheduled Script/Map-Reduce instead. [src1]
- HTTP status codes are NOT controllable by RESTlet code — a normal return always produces 200 OK; a thrown error always produces 400 Bad Request. You cannot return 201, 204, 404, or 422 from a RESTlet. [src3]
- Account-level concurrency is shared between RESTlets AND SOAP web services AND REST Web Services — high SOAP volume reduces available RESTlet slots. [src4]
- RESTlets cannot render HTML pages or serve files directly — use Suitelets for browser-facing endpoints. [src6]
- External HTTP callouts from within a RESTlet (via N/http module) count 10 governance units each and are limited to synchronous execution — no async callouts within a single RESTlet invocation. [src1]
- RESTlets do not support WebSocket or long-polling — they are strictly request-response with no persistent connections. [src2]
- File Cabinet file access from RESTlets is limited to 10 MB per file load operation. [src1]
- RESTlet deployment must be set to "Released" status before external callers can access the endpoint. "Testing" status only allows calls from within the NetSuite UI debugger. [src5]

## 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 Point | HTTP Method | Parameter Type | Typical Use | Notes |
|---|---|---|---|---|
| `get(requestParams)` | GET | URL query parameters (parsed as object) | Retrieve records, run searches | Parameters auto-parsed to key-value object [src2] |
| `post(requestBody)` | POST | JSON request body (parsed as object) | Create records, execute operations | Body must be valid JSON [src2] |
| `put(requestBody)` | PUT | JSON request body (parsed as object) | Update/upsert records | Body must be valid JSON [src2] |
| `delete(requestParams)` | DELETE | URL query parameters (parsed as object) | Delete records | Parameters 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 you want to support. [src2, src5]

```javascript
/**
 * @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 };
    }

    function put(requestBody) {
        log.debug('PUT body', JSON.stringify(requestBody));
        var rec = record.load({ type: record.Type.CUSTOMER, id: requestBody.id });
        if (requestBody.companyName) rec.setValue('companyname', requestBody.companyName);
        if (requestBody.email) rec.setValue('email', requestBody.email);
        rec.save();
        return { success: true, id: requestBody.id };
    }

    function doDelete(requestParams) {
        log.debug('DELETE params', JSON.stringify(requestParams));
        record.delete({ type: record.Type.CUSTOMER, id: requestParams.customerId });
        return { success: true };
    }

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

**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, and save the Script Record. Then create a Script Deployment with Status = Released. [src5]

```
Script Record:
  Name:        "Customer RESTlet"
  Script File:  SuiteScripts/customer_restlet.js
  Script ID:    _customer_restlet (auto-generated if left blank)

Deployment Record:
  Title:        "Customer RESTlet Deployment"
  Deployed:     Yes
  Status:       Released    <-- MUST be Released for external access
  Execute As:   <Integration Role>
  Audience:     All Roles (or specific roles)
  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]

```bash
# TBA-authenticated GET request
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=\"CONSUMER_KEY\",oauth_token=\"TOKEN_ID\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"TIMESTAMP\",oauth_nonce=\"NONCE\",oauth_version=\"1.0\",oauth_signature=\"SIGNATURE\"" \
  -H "Content-Type: application/json"
```

**Verify**: Response is `200 OK` with JSON body: `{"id":"12345","companyName":"Acme Corp","email":"info@acme.com","phone":"555-0100"}`

## Code Examples

### Python: Call a NetSuite RESTlet with TBA Authentication

```python
# 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
ACCOUNT_ID = "YOUR_ACCOUNT_ID"
CONSUMER_KEY = "YOUR_CONSUMER_KEY"
CONSUMER_SECRET = "YOUR_CONSUMER_SECRET"
TOKEN_KEY = "YOUR_TOKEN_ID"
TOKEN_SECRET = "YOUR_TOKEN_SECRET"

RESTLET_URL = (
    f"https://{ACCOUNT_ID}.restlets.api.netsuite.com"
    f"/app/site/hosting/restlet.nl?script=123&deploy=1"
)

auth = OAuth1(
    client_key=CONSUMER_KEY,
    client_secret=CONSUMER_SECRET,
    resource_owner_key=TOKEN_KEY,
    resource_owner_secret=TOKEN_SECRET,
    realm=ACCOUNT_ID,
    signature_method="HMAC-SHA256",  # NetSuite requires HMAC-SHA256
)

# GET example: retrieve a customer
response = requests.get(
    RESTLET_URL + "&customerId=12345",
    auth=auth,
    headers={"Content-Type": "application/json"},
    timeout=30,
)
response.raise_for_status()
print(response.json())

# POST example: create a customer
payload = {"companyName": "Acme Corp", "email": "info@acme.com"}
response = requests.post(
    RESTLET_URL,
    json=payload,
    auth=auth,
    headers={"Content-Type": "application/json"},
    timeout=30,
)
# Note: 200 = success, 400 = RESTlet threw an error
if response.status_code == 200:
    print("Created:", response.json())
else:
    print("Error:", response.text)
```

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

```javascript
// Input:  NetSuite credentials, RESTlet URL, record data
// Output: JSON response from RESTlet

// Pin: oauth-1.0a@2.2.6, crypto (built-in)
const OAuth = require('oauth-1.0a');
const crypto = require('crypto');
const https = require('https');

const ACCOUNT_ID = 'YOUR_ACCOUNT_ID';
const RESTLET_URL = `https://${ACCOUNT_ID}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1`;

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' };

async function callRestlet(method, params = {}, body = null) {
  const url = new URL(RESTLET_URL);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));

  const authHeader = oauth.toHeader(
    oauth.authorize({ url: url.toString(), method }, token)
  );
  authHeader.Authorization += `, realm="${ACCOUNT_ID}"`;

  const response = await fetch(url.toString(), {
    method,
    headers: {
      ...authHeader,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
  });

  const data = await response.json();
  if (response.status !== 200) throw new Error(`RESTlet error: ${JSON.stringify(data)}`);
  return data;
}

// Usage
callRestlet('GET', { customerId: '12345' }).then(console.log);
callRestlet('POST', {}, { companyName: 'Acme Corp', email: 'info@acme.com' }).then(console.log);
```

### cURL: Quick RESTlet test (OAuth 2.0)

```bash
# Input:  OAuth 2.0 access token (obtained via token endpoint)
# 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"

# Step 3: POST to create a record
curl -X POST \
  "https://ACCOUNT_ID.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companyName":"Acme Corp","email":"info@acme.com"}'
```

## Data Mapping

### RESTlet Input/Output Data Types

| SuiteScript Type | JSON Representation | Input Parsing | Output Serialization | Gotcha |
|---|---|---|---|---|
| String | `"value"` | Direct | Direct | NetSuite internally trims whitespace on some fields |
| Integer (internal ID) | `12345` (number) | Auto-parsed from JSON | Returns as number | Must be numeric — string IDs cause silent failures on record.load() |
| Boolean | `true`/`false` | NetSuite uses `"T"`/`"F"` internally | RESTlet can return native booleans | `setValue('field', true)` works; `getValue()` returns `"T"` not `true` |
| Date | `"3/1/2026"` | Must match user date format preference | Returns as formatted string | Date format depends on the execution user's preferences — not ISO 8601 by default |
| Currency | `99.99` (number) | Auto-parsed | Returns as number | Precision depends on currency setup; multi-currency accounts need explicit conversion |
| Multi-select | `[1, 2, 3]` (array of IDs) | Array of internal IDs | Returns array | Must use `setValue` with array; individual `setValue` calls overwrite |
| Sublist line items | Nested array | Must iterate with `setSublistValue()` per line | Must iterate with `getSublistValue()` | No bulk sublist set — each line costs governance units |

### Data Type Gotchas

- Date values in RESTlets follow the logged-in user's date format preference, NOT ISO 8601 — if your integration user has MM/DD/YYYY format but your system sends YYYY-MM-DD, dates will be misinterpreted silently. Use `format.parse()` from the N/format module to ensure correct parsing. [src2]
- Internal IDs are integers but the N/search module sometimes returns them as strings — always parseInt() before passing to record.load(). [src2]
- Currency amounts in NetSuite are stored with the currency's native precision — JPY has 0 decimal places, USD has 2. Sending `99.999` for a USD field silently rounds to `100.00`. [src7]
- Multi-select fields via RESTlet must send an array of internal IDs, not labels or text values. Getting available options requires a separate search or record.getField().getSelectOptions() call. [src2]

## Error Handling & Failure Points

### Common Error Codes

| HTTP Code | SuiteScript Error Code | Meaning | Cause | Resolution |
|---|---|---|---|---|
| 400 | `UNEXPECTED_ERROR` | Unhandled runtime error in RESTlet | Script threw an uncaught exception | Wrap RESTlet logic in try/catch; return structured error in response body [src3] |
| 400 | `SSS_INVALID_SCRIPTLET_ID` | RESTlet not found | Wrong script/deploy ID in URL, or deployment not Released | Verify script ID, deploy ID, and deployment status = Released [src3] |
| 401 | `INVALID_LOGIN_ATTEMPT` | Authentication failed | Bad OAuth signature, expired token, or wrong account ID | Regenerate OAuth signature; verify consumer key, token, and account realm [src3] |
| 403 | `FORBIDDEN` | Request sent to wrong domain | RESTlet URL pointed at wrong data center | Use dynamic URL from deployment record, not hardcoded [src3] |
| 405 | `METHOD_NOT_ALLOWED` | HTTP method not supported | RESTlet doesn't export an entry point for the HTTP method used | Add the missing entry point function (get/post/put/delete) to the script [src3] |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | Wrong Content-Type header | Content-Type not application/json or text/plain | Set Content-Type: application/json header [src3] |
| 302 | `MOVED_TEMPORARILY` | Wrong data center | Request routed to incorrect NetSuite data center | Regenerate RESTlet URL from deployment record [src3] |
| 503 | `SERVICE_UNAVAILABLE` | NetSuite database offline | Maintenance window or database connectivity issue | Retry with exponential backoff; check status.netsuite.com [src3] |

### Failure Points in Production

- **Governance exhaustion on large operations**: Processing >200 transaction records in a single RESTlet call can exhaust the 5,000 unit budget. Fix: `Pre-calculate governance cost — (records * unit_cost_per_operation) must be < 5,000. Split large batches into multiple RESTlet calls.` [src1]
- **Silent data truncation on 10 MB payloads**: Payloads near the 10 MB limit may be truncated without error on certain NetSuite releases. Fix: `Chunk large payloads into <5 MB batches with pagination tokens. Validate response completeness on the caller side.` [src1]
- **Concurrency exhaustion under load**: If all 5 per-user (or account-level) slots are occupied, subsequent RESTlet calls queue and eventually timeout. Fix: `Implement client-side concurrency throttling with semaphore pattern. Monitor via Setup > Integration > Web Services Usage Log.` [src4]
- **Date format mismatches**: Integration user's date preference differs from the external system's format, causing dates to be silently misinterpreted. Fix: `Always use N/format.parse() and N/format.format() in RESTlet code. Standardize integration user's preferences to MM/DD/YYYY or use epoch timestamps.` [src2]
- **OAuth signature failures after password/role changes**: Changing the integration user's password or role invalidates TBA tokens. Fix: `Use a dedicated integration user whose credentials never change. Set up monitoring for 401 errors.` [src7]
- **Deployment status reverted after bundle update**: Installing or updating SuiteCloud bundles can revert RESTlet deployment status to Testing. Fix: `After any bundle install/update, verify all RESTlet deployments are still Released.` [src5]

## Anti-Patterns

### Wrong: Returning HTTP status codes for error semantics

```javascript
// BAD -- you CANNOT control HTTP status codes in RESTlets
function post(requestBody) {
    var rec = record.load({ type: 'customer', id: requestBody.id });
    if (!rec) {
        // This does NOT return 404 -- it returns 200 with the object below
        return { status: 404, message: 'Customer not found' };
    }
    return rec;
}
```

### Correct: Use structured response body for error handling

```javascript
// 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) {
        // throw produces HTTP 400; return produces HTTP 200
        // Choose based on whether caller can parse response body
        return { success: false, error: { code: e.name, message: e.message } };
    }
}
```

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

```javascript
// BAD -- record.load() costs 5-10 governance units; loading 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

```javascript
// GOOD -- search.lookupFields costs 1 unit; search.create costs 10 units for any number of results
function get(requestParams) {
    var ids = requestParams.ids.split(',');

    // Option A: lookupFields (1 unit per call, best for <10 records)
    if (ids.length < 10) {
        return ids.map(function(id) {
            var fields = search.lookupFields({
                type: search.Type.CUSTOMER, id: id,
                columns: ['companyname', 'email']
            }); // 1 unit each
            return { id: id, name: fields.companyname, email: fields.email };
        });
    }

    // Option B: search.create (10 units total, best for 10+ records)
    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'),
            email: result.getValue('email')
        });
        return true;
    });
    return results; // 10 units total regardless of result count
}
```

### Wrong: Not implementing idempotency for POST/PUT operations

```javascript
// BAD -- duplicate POST requests create duplicate records
function post(requestBody) {
    var rec = record.create({ type: record.Type.SALES_ORDER });
    rec.setValue('entity', requestBody.customerId);
    // ... set other fields
    var id = rec.save(); // Creates a NEW record every time
    return { id: id };
}
```

### Correct: Use external ID or idempotency key to prevent duplicates

```javascript
// GOOD -- check for existing record using external ID before creating
function post(requestBody) {
    var externalId = requestBody.externalOrderId;

    // Check if record already exists (1 governance unit)
    var existing = search.lookupFields({
        type: 'salesorder',
        id: externalId,
        columns: ['internalid']
    });
    if (existing && existing.internalid) {
        return { success: true, id: existing.internalid, created: false };
    }

    // Create new record with external ID
    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

- **Hardcoding the RESTlet URL domain**: NetSuite data centers can change (e.g., during migrations). The RESTlet URL domain portion must be dynamically resolved from the Script Deployment record, not hardcoded. Fix: `Use the URL from the deployment record's External URL field. Implement 302 redirect following in your HTTP client.` [src3]
- **Ignoring governance unit budget**: Each SuiteScript API call costs governance units. A RESTlet that loads 100 transaction records (10 units each) already costs 1,000 units before any processing. Fix: `Pre-calculate governance cost before execution. Use search.lookupFields (1 unit) instead of record.load (5-10 units) when you only need a few fields.` [src1]
- **Testing only in sandbox with low data volume**: Sandbox accounts have identical governance limits but less data — operations that use 500 governance units in sandbox may use 4,500 in production with 10x the data volume. Fix: `Load-test with production-scale data volumes. Use script.getRemainingUsage() to monitor governance consumption.` [src1]
- **Not handling the 302 redirect**: NetSuite returns 302 when the request hits the wrong data center. Many HTTP clients follow redirects automatically but lose the OAuth Authorization header on redirect. Fix: `Disable auto-redirect in your HTTP client. On 302, re-sign the request with the new URL and retry.` [src3]
- **Mixing SuiteScript 1.0 and 2.x patterns**: SuiteScript 1.0 RESTlets use different entry point signatures and governance rules. Code copied from 1.0 examples fails silently or throws unexpected errors in 2.x. Fix: `Always verify the @NApiVersion annotation is 2.x. Use only N/ module imports (not nlapiXxx functions).` [src2]
- **Forgetting to set Content-Type header**: Omitting Content-Type: application/json causes NetSuite to reject the request with 415 Unsupported Media Type or to misparse the request body. Fix: `Always include Content-Type: application/json in request headers.` [src3]

## Diagnostic Commands

```bash
# 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=SCRIPT_ID&deploy=DEPLOY_ID" \
  -H "Authorization: OAuth realm=\"ACCOUNT_ID\",oauth_consumer_key=\"CK\",oauth_token=\"TK\",oauth_signature_method=\"HMAC-SHA256\",oauth_timestamp=\"$(date +%s)\",oauth_nonce=\"$(openssl rand -hex 16)\",oauth_version=\"1.0\",oauth_signature=\"SIG\"" \
  -H "Content-Type: application/json"
# Expected: 200 OK (or 401 if credentials wrong)

# Check remaining governance units (from within SuiteScript)
# Add to your RESTlet: log.debug('Remaining', runtime.getCurrentScript().getRemainingUsage());
# Then check: Customization > Scripting > Script Execution Log

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

# Monitor concurrency usage
# Navigate to: Setup > Integration > Web Services Usage Log > Concurrency tab
# Shows: concurrent requests over time, peak usage, throttled requests

# Check NetSuite system status (outage/maintenance)
# Visit: https://status.netsuite.com
```

## Version History & Compatibility

| SuiteScript Version | Release | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| SuiteScript 2.1 | 2020.1 | Current | ES2019 syntax supported (async/await, arrow functions) | Use `@NApiVersion 2.1` annotation; all 2.0 code is compatible |
| SuiteScript 2.0 | 2015.2 | Supported | Module-based architecture (define/require) | N/ module imports replace nlapiXxx functions |
| SuiteScript 1.0 | Legacy | Maintenance-only | Different entry point signatures, different governance model | Not recommended for new development; 1.0 governance has recovery points (2.x does not) |

Account-level concurrency governance was introduced in **2017.2** and is the most significant change affecting RESTlet design patterns — prior to 2017.2, concurrency was per-user and per-authentication-method. [src4]

### Deprecation Policy

Oracle NetSuite generally 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 release notes (quarterly releases: .1 and .2 each year). Breaking changes are rare and typically announced 2 releases in advance. [src2]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| You need custom business logic executed within NetSuite's context | You only need standard record CRUD (create/read/update/delete) | NetSuite REST Web Services (no code required) |
| You need to combine multiple operations in a single atomic call | You need to process >500 records per operation | Scheduled Script or Map/Reduce (10,000 governance units) |
| External systems need to push data into NetSuite in real-time | You need to serve HTML pages or forms to end users | Suitelets (can render HTML, serve files) |
| You need custom response formats (aggregated data, computed fields) | You need file-based bulk import (CSV, XML) | CSV Import or SuiteTalk SOAP |
| You're building a webhook receiver for external event notifications | You need long-running operations (>30 seconds) | Scheduled Script with status tracking |
| You need to expose NetSuite data to a non-NetSuite application | You need real-time change notifications FROM NetSuite | User Event Scripts + N/http callout |

## Important Caveats

- Governance unit costs may change between NetSuite releases — always verify against the current [SuiteScript 2.x API Governance](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157072844224.html) page for your release version. [src1]
- Account-level concurrency varies dramatically by NetSuite tier and SuiteCloud Plus licensing — a base account may have as few as 5 concurrent slots shared across ALL integration methods. [src4]
- RESTlet performance in sandbox environments may differ from production due to different hardware allocation and data volumes — always performance-test with production-representative data. [src7]
- OAuth 2.0 support for RESTlets was added relatively recently and may not be available on all NetSuite account types — check your account's Integration Management settings. [src2]
- SuiteScript 2.1 (with ES2019 support including async/await) is available for RESTlets but requires the `@NApiVersion 2.1` annotation — 2.0 scripts cannot use ES2019 syntax. [src2]
- The 5,000 governance unit limit is per RESTlet invocation, not per HTTP request — if a RESTlet is called internally from another SuiteScript, each call gets its own 5,000 unit budget. [src1]

## Related Units

- [NetSuite REST Web Services](https://knowledgelib.io/business/erp-integration/netsuite-rest-api-capabilities/2026) — Built-in REST API for standard record CRUD without custom code
- [NetSuite SuiteTalk SOAP API](https://knowledgelib.io/business/erp-integration/netsuite-suitetalk-soap-capabilities/2026) — Legacy SOAP integration surface
- [NetSuite Suitelets](https://knowledgelib.io/business/erp-integration/netsuite-suitelet-capabilities/2026) — Custom pages and forms (1,000 governance units, browser-facing)
- [NetSuite-Shopify Order Sync](https://knowledgelib.io/business/erp-integration/netsuite-shopify-order-sync/2026) — Integration playbook that uses RESTlets
