OAuth 2.0 Setup Guide for NetSuite
This comprehensive guide walks you through implementing OAuth 2.0 authentication in NetSuite, from initial configuration to testing with Postman.
Overview
What is OAuth 2.0?
OAuth 2.0 is the industry-standard protocol for authorization. NetSuite supports two OAuth 2.0 flows:
| Flow | Use Case | Description |
|---|---|---|
| Client Credentials | Machine-to-Machine | Server-to-server integration without user interaction |
| Authorization Code | User Context | User grants access to their NetSuite data |
OAuth 2.0 Flow Diagram
Prerequisites
Before starting, ensure you have:
- NetSuite Administrator role or equivalent permissions
- Access to Setup > Company > Enable Features
- OpenSSL installed (or Git Bash on Windows)
- Postman installed for testing
Setup Checklist & Results Summary
Use this checklist to track your progress and understand how each step's output is used later.
| Step | Action | Result | Used In |
|---|---|---|---|
| 1 | Enable OAuth 2.0 Feature | ✅ OAuth 2.0 & REST Web Services enabled | Step 2 (allows integration creation) |
| 2 | Create Integration Record | Consumer Key (Client ID) | Step 6 (Scheduler config) |
Consumer Secret | Store securely (backup only) | ||
| 3.1 | Generate RSA Keypair | private_key.pem | Step 6 (upload to File Cabinet) |
public_cert.pem | Step 3.3 (upload to NetSuite) | ||
| 3.2 | Verify Certificate & Key | ✅ Keys validated | - |
| 3.3 | Upload Certificate to NetSuite | ✅ Certificate registered | - |
| 3.4 | Note Certificate ID | Certificate ID | Step 6 (Scheduler config) |
| 4 | Find NetSuite URLs | Account ID | Step 6 (Scheduler config) |
Token Endpoint URL | Step 6 (token request) | ||
REST API Base URL | Step 8 (API calls) | ||
| 5 | Deploy Token Proxy Scripts | Scheduler + Suitelet deployed | Step 6 |
| 6 | Configure Scheduler Parameters | ✅ All config values set | Step 7 |
| 7 | Test Scheduler Execution | ✅ Bearer token generated | Step 8 |
| 8 | Test with Postman | ✅ API call successful | Production ready |
Quick Reference: Values You'll Need
┌────────────────────────────────────────────────────────────────────────┐
│ OAUTH 2.0 CONFIGURATION VALUES │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ From Step 2 (Integration Record): │
│ ├─ Consumer Key (Client ID): ____________________________ │
│ └─ Consumer Secret: ____________________________ │
│ │
│ From Step 3 (Certificate): │
│ ├─ private_key.pem location: ____________________________ │
│ └─ Certificate ID: ____________________________ │
│ │
│ From Step 4 (URLs): │
│ ├─ Account ID: ____________________________ │
│ ├─ Token URL: https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/ │
│ │ services/rest/auth/oauth2/v1/token │
│ └─ API URL: https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/ │
│ services/rest/query/v1/suiteql │
│ │
└────────────────────────────────────────────────────────────────────────┘
Dependency Flow Diagram
Step 1: Enable OAuth 2.0 Feature
First, enable OAuth 2.0 in your NetSuite account.
┌──────────────────────────────────────────────────────────────────────────┐
│ ENABLE OAUTH 2.0 FEATURE │
└──────────────────────────────────────────────────────────────────────────┘
Navigate to:
Setup → Company → Enable Features → SuiteCloud
Enable:
┌────────────────────────────────────────────────────────────────────┐
│ ☑ OAuth 2.0 │
│ ☑ REST Web Services │
│ ☑ SuiteScript (if using RESTlets) │
└────────────────────────────────────────────────────────────────────┘
Detailed Steps:
- Go to Setup → Company → Enable Features
- Click on the SuiteCloud subtab
- Under SuiteScript, ensure these are checked:
- ☑ Client SuiteScript
- ☑ Server SuiteScript
- Under Manage Authentication, check:
- ☑ OAuth 2.0
- Under SuiteTalk (Web Services), check:
- ☑ REST Web Services
- Click Save
You must have the Administrator role to enable these features. Changes take effect immediately.
What you now have:
- ✅ OAuth 2.0 feature enabled
- ✅ REST Web Services enabled
Next: Step 2 - Create Integration Record
Step 2: Create Integration Record
The integration record identifies your external application to NetSuite.
┌──────────────────────────────────────────────────────────────────────────┐
│ CREATE INTEGRATION RECORD │
└──────────────────────────────────────────────────────────────────────────┘
Navigate to:
Setup → Integration → Manage Integrations → New
Integration Record Configuration
| Field | Value | Notes |
|---|---|---|
| Name | My OAuth2 Integration | Descriptive name |
| State | Enabled | Must be enabled |
| OAuth 2.0 | ☑ Checked | Enable OAuth 2.0 for this integration |
| Token-Based Authentication | ☐ Unchecked | Not needed for OAuth 2.0 |
| Authorization Code Grant | ☑ or ☐ | Check if using Authorization Code flow |
| Client Credentials (M2M) | ☑ Checked | Required for machine-to-machine |
| REST Web Services | ☑ Checked | Enable REST API access |
Detailed Steps:
- Go to Setup → Integration → Manage Integrations
- Click New
- Fill in the form:
┌────────────────────────────────────────────────────────────────────────┐
│ INTEGRATION RECORD FORM │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Name: [My OAuth2 Integration ] │
│ │
│ Description: [OAuth 2.0 integration for external API access] │
│ │
│ State: [● Enabled ○ Blocked] │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ AUTHENTICATION │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ ☑ OAuth 2.0 │
│ ├─ ☑ Client Credentials (Machine to Machine) Grant │
│ ├─ ☐ Authorization Code Grant │
│ └─ Callback URL: [ ] │
│ │
│ ☐ Token-Based Authentication │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ ACCESS │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ ☑ REST Web Services │
│ ☐ User Access Tokens │
│ │
└────────────────────────────────────────────────────────────────────────┘
- Click Save
After Saving - IMPORTANT!
After saving, NetSuite displays your credentials ONLY ONCE:
┌────────────────────────────────────────────────────────────────────────┐
│ ⚠️ SAVE THESE CREDENTIALS IMMEDIATELY │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Consumer Key / Client ID: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ Consumer Secret / Client Secret: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4j3i2h1g0f9e8d7c6b5a4z3y2x1w0 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ Copy these now! They cannot be retrieved later. │
│ │
└────────────────────────────────────────────────────────────────────────┘
Copy and securely store the Consumer Key and Consumer Secret immediately. They are shown only once and cannot be retrieved later. If lost, you must create a new integration.
What you now have:
Consumer Key (Client ID)→ Used in Step 3.3 and Step 5 (JWTissclaim)Consumer Secret→ Store securely as backup
Next: Step 3 - Create OAuth 2.0 Certificate
Step 3: Create OAuth 2.0 Certificate
For Client Credentials flow, you need a certificate keypair (public certificate + private key) to sign your requests.
Generate your own keypair locally using OpenSSL and upload the public certificate to NetSuite. This approach ensures:
- ✅ Private key never leaves your machine
- ✅ Full control over key storage
- ✅ Can use existing key management systems
- ✅ Easier key rotation
Step 3.1: Generate RSA Keypair with X.509 Certificate
# Generate 4096-bit RSA private key and self-signed X.509 certificate (valid for 2 years)
openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_cert.pem -nodes -days 730
# When prompted, enter certificate details (or press Enter for defaults)
# Common Name (CN) can be your integration name, e.g., "NetSuite Integration"
# Verify the files were created
ls -la *.pem
If OpenSSL is not installed, you can use Git Bash (comes with Git for Windows) which includes OpenSSL.
Note: In Git Bash, use double slashes for the subject parameter to avoid path conversion:
openssl req -x509 -newkey rsa:4096 -keyout private_key.pem -out public_cert.pem -nodes -days 730 -subj "//CN=NetSuite Integration"
Step 3.2: Verify Generated Certificate and Key
# View private key info (don't share this!)
openssl rsa -in private_key.pem -check -noout
# View certificate info
openssl x509 -in public_cert.pem -text -noout
Expected output for private key:
RSA key ok
Expected output for certificate (truncated):
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ...
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = NetSuite Integration
Validity
Not Before: ...
Not After : ...
Subject: CN = NetSuite Integration
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Step 3.3: Upload Certificate to NetSuite
┌──────────────────────────────────────────────────────────────────────────┐
│ UPLOAD CERTIFICATE TO NETSUITE │
└──────────────────────────────────────────────────────────────────────────┘
Navigate to:
Setup → Integration → OAuth 2.0 Client Credentials (M2M) Setup
- Go to Setup → Integration → OAuth 2.0 Client Credentials (M2M) Setup
- Click Create New
- Fill in the form:
┌────────────────────────────────────────────────────────────────────────┐
│ OAUTH 2.0 CLIENT CREDENTIALS SETUP │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Entity: [Select User/Employee ▼] │
│ Role: [Select Role (e.g., Administrator) ▼] │
│ Application: [My OAuth2 Integration ▼] │
│ │
│ ───────────────────────────────────────────────────────────────── │
│ CERTIFICATE │
│ ───────────────────────────────────────────────────────────────── │
│ │
│ Public Key Certificate: │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ [Choose File] public_cert.pem │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ - OR paste PEM content: - │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ -----BEGIN CERTIFICATE----- │ │
│ │ MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA... │ │
│ │ ... │ │
│ │ -----END CERTIFICATE----- │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
- Either:
- Click Choose File and select your
public_cert.pem, OR - Copy/paste the contents of
public_cert.peminto the text area
- Click Choose File and select your
- Click Save
Step 3.4: Note the Certificate ID
After saving, NetSuite assigns a Certificate ID:
┌────────────────────────────────────────────────────────────────────────┐
│ CERTIFICATE CREATED SUCCESSFULLY │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Certificate ID: 1234567 ← SAVE THIS! │
│ │
│ Entity: John Developer │
│ Role: Administrator │
│ Application: My OAuth2 Integration │
│ Public Key: Uploaded │
│ │
└────────────────────────────────────────────────────────────────────────┘
What you now have:
private_key.pem→ Used in Step 6 (upload to File Cabinet)public_cert.pem→ Already uploaded to NetSuiteCertificate ID→ Used in Step 6 (Scheduler config)
Collected so far:
| Value | From Step | Used In |
|---|---|---|
| Consumer Key (Client ID) | Step 2 | Step 6 (Scheduler config) |
| private_key.pem | Step 3.1 | Step 6 (File Cabinet) |
| Certificate ID | Step 3.4 | Step 6 (Scheduler config) |
Next: Step 4 - Understand Your NetSuite URLs
Step 4: Understand Your NetSuite URLs
You need these URLs for OAuth 2.0:
Account-Specific URLs
┌────────────────────────────────────────────────────────────────────────┐
│ NETSUITE OAUTH 2.0 ENDPOINTS │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Token Endpoint (for getting access tokens): │
│ https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/ │
│ oauth2/v1/token │
│ │
│ REST API Base URL: │
│ https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/ │
│ │
│ SuiteQL Endpoint: │
│ https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/ │
│ query/v1/suiteql │
│ │
└────────────────────────────────────────────────────────────────────────┘
How to Find Your Account ID
Your Account ID format depends on your NetSuite environment:
| Environment | Account ID Format | Example |
|---|---|---|
| Production | Numeric or with suffix | 1234567 or 1234567_SB1 |
| Sandbox | With _SB# suffix | 1234567_SB1, 1234567_SB2 |
To find your Account ID:
- Go to Setup → Company → Company Information
- Look for Account ID field
Or check your NetSuite URL:
https://1234567.app.netsuite.com/...
↑↑↑↑↑↑↑
Account ID
What you now have:
Account ID→ Used in Step 6 (Scheduler config)Token Endpoint URL→ Used in Step 6 (token request)REST API Base URL→ Used in Step 8 (API calls)
All values collected - Ready for Step 6:
| Value | From Step | Used In Step 6 |
|---|---|---|
| Consumer Key (Client ID) | Step 2 | Scheduler CONFIG |
| private_key.pem | Step 3.1 | Upload to File Cabinet |
| Certificate ID | Step 3.4 | Scheduler CONFIG |
| Account ID | Step 4 | Scheduler CONFIG |
Next: Step 5 - Deploy Token Proxy Scripts
Step 5: Implement Token Proxy Pattern (Suitelet + Scheduler)
For scenarios where external systems cannot generate JWTs or manage private keys, you can implement a Token Proxy Pattern using a Suitelet and Scheduled Script within NetSuite.
Architecture Overview
When to Use This Pattern
| Use Case | Recommended? |
|---|---|
| External system can't manage private keys | ✅ Yes |
| Simple internal tools/dashboards | ✅ Yes |
| Single trusted external system | ✅ Yes |
| Multiple untrusted external systems | ❌ No - use standard OAuth |
| High security requirements | ❌ No - use standard OAuth |
Implementation Components
1. Custom Record for Token Storage (Recommended)
Create a custom record to store the token:
| Field | Type | Description |
|---|---|---|
custrecord_oauth_token | Text Area | The access token |
custrecord_oauth_expires | Date/Time | Token expiration timestamp |
custrecord_oauth_updated | Date/Time | Last refresh time |
custrecord_oauth_status | List | Active/Error/Expired |
2. Scheduled Script (Token Refresher)
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
* @NModuleScope SameAccount
*/
define(['N/https', 'N/record', 'N/encode', 'N/crypto', 'N/file'],
function(https, record, encode, crypto, file) {
const CONFIG = {
CLIENT_ID: 'your_consumer_key_here',
CERTIFICATE_ID: 'your_certificate_id_here',
ACCOUNT_ID: 'your_account_id_here',
PRIVATE_KEY_FILE_ID: 12345, // File Cabinet ID of private_key.pem
TOKEN_RECORD_ID: 1 // Custom record ID to update
};
function execute(context) {
try {
// 1. Load private key from File Cabinet
const privateKeyFile = file.load({ id: CONFIG.PRIVATE_KEY_FILE_ID });
const privateKey = privateKeyFile.getContents();
// 2. Generate JWT
const jwt = generateJWT(privateKey);
// 3. Request access token
const tokenResponse = requestAccessToken(jwt);
// 4. Store token in custom record
storeToken(tokenResponse);
log.audit('Token Refresh', 'Access token refreshed successfully');
} catch (e) {
log.error('Token Refresh Failed', e.message);
// Optionally send alert email
}
}
function generateJWT(privateKey) {
const tokenUrl = `https://${CONFIG.ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`;
const now = Math.floor(Date.now() / 1000);
const exp = now + 3600;
const header = {
typ: 'JWT',
alg: 'RS256',
kid: CONFIG.CERTIFICATE_ID
};
const payload = {
iss: CONFIG.CLIENT_ID,
scope: ['rest_webservices'],
aud: tokenUrl,
exp: exp,
iat: now
};
// Base64URL encode
const headerB64 = base64UrlEncode(JSON.stringify(header));
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
const signingInput = headerB64 + '.' + payloadB64;
// Sign with RS256
const signer = crypto.createSigner({
algorithm: crypto.HashAlg.SHA256,
key: crypto.createSecretKey({ secret: privateKey })
});
signer.update({ input: signingInput });
const signature = signer.sign({ outputEncoding: encode.Encoding.BASE_64_URL_SAFE });
return signingInput + '.' + signature;
}
function requestAccessToken(jwt) {
const tokenUrl = `https://${CONFIG.ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`;
const response = https.post({
url: tokenUrl,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=${jwt}`
});
if (response.code !== 200) {
throw new Error('Token request failed: ' + response.body);
}
return JSON.parse(response.body);
}
function storeToken(tokenResponse) {
const tokenRecord = record.load({
type: 'customrecord_oauth_token',
id: CONFIG.TOKEN_RECORD_ID
});
tokenRecord.setValue({
fieldId: 'custrecord_oauth_token',
value: tokenResponse.access_token
});
tokenRecord.setValue({
fieldId: 'custrecord_oauth_expires',
value: new Date(Date.now() + (tokenResponse.expires_in * 1000))
});
tokenRecord.setValue({
fieldId: 'custrecord_oauth_updated',
value: new Date()
});
tokenRecord.setValue({
fieldId: 'custrecord_oauth_status',
value: 1 // Active
});
tokenRecord.save();
}
function base64UrlEncode(str) {
const base64 = encode.convert({
string: str,
inputEncoding: encode.Encoding.UTF_8,
outputEncoding: encode.Encoding.BASE_64
});
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
return { execute };
});
Deployment Configuration:
- Schedule: Every 15 minutes
- Status: Released
3. Suitelet (Token Endpoint)
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
* @NModuleScope SameAccount
*/
define(['N/record', 'N/log'],
function(record, log) {
// ============================================
// Configuration
// ============================================
const CONFIG = {
TOKEN_RECORD_ID: 1,
// Hardcoded credentials for authentication
VALID_CREDENTIALS: {
'integration_user': 'SecureP@ssw0rd123',
'external_app': 'An0therS3cretKey!'
}
};
function onRequest(context) {
const response = context.response;
response.setHeader({ name: 'Content-Type', value: 'application/json' });
try {
// 1. Validate Username & Password
const username = context.request.parameters.username ||
context.request.headers['X-Username'];
const password = context.request.parameters.password ||
context.request.headers['X-Password'];
if (!validateCredentials(username, password)) {
log.audit('Auth Failed', 'Invalid credentials from: ' + username);
response.write(JSON.stringify({
error: 'unauthorized',
message: 'Invalid username or password'
}));
return;
}
// 2. Load token from custom record
const tokenRecord = record.load({
type: 'customrecord_oauth_token',
id: CONFIG.TOKEN_RECORD_ID
});
const accessToken = tokenRecord.getValue({ fieldId: 'custrecord_oauth_token' });
const expiresAt = tokenRecord.getValue({ fieldId: 'custrecord_oauth_expires' });
const status = tokenRecord.getValue({ fieldId: 'custrecord_oauth_status' });
// 3. Check if token is valid
if (status !== 1 || new Date() > expiresAt) {
response.write(JSON.stringify({
error: 'token_expired',
message: 'Token has expired, please wait for refresh'
}));
return;
}
// 4. Return token
response.write(JSON.stringify({
access_token: accessToken,
token_type: 'Bearer',
expires_at: expiresAt.toISOString()
}));
log.audit('Token Requested', 'Token provided to: ' + username);
} catch (e) {
log.error('Token Endpoint Error', e.message);
response.write(JSON.stringify({
error: 'server_error',
message: 'Failed to retrieve token'
}));
}
}
/**
* Validate username and password against hardcoded credentials
*/
function validateCredentials(username, password) {
if (!username || !password) {
return false;
}
return CONFIG.VALID_CREDENTIALS[username] === password;
}
return { onRequest };
});
Deployment Configuration:
- Status: Released
- Execute As Role: Role with REST permissions
- Available Without Login: ✅ Yes (for external access)
Usage from External System
Suitelet External URL Format
When deployed with Available Without Login enabled, NetSuite provides an external URL:
┌────────────────────────────────────────────────────────────────────────┐
│ EXTERNAL SUITELET URL STRUCTURE │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ https://{ACCOUNT_ID}.extforms.netsuite.com/app/site/hosting/ │
│ scriptlet.nl?script={SCRIPT_ID}&deploy={DEPLOY_ID}&compid={ACCOUNT} │
│ &ns-at={AUTH_TOKEN}&username={USER}&password={PASS} │
│ │
└────────────────────────────────────────────────────────────────────────┘
| Parameter | Description | Example |
|---|---|---|
{ACCOUNT_ID} | NetSuite account ID | 7051733_SB1 |
script | Script internal ID | 350 |
deploy | Deployment ID | 1 |
compid | Company/Account ID | 7051733_SB1 |
ns-at | NetSuite auth token (auto-generated) | AAEJ7tMQ... |
username | Your integration username | integration_user |
password | Your integration password | SecureP@ssw0rd123 |
Example Request
# Request token from external Suitelet
curl -X GET \
"https://7051733-sb1.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=350&deploy=1&compid=7051733_SB1&ns-at=AAEJ7tMQhgzW1OmUhus5KwaB7XwSQ2mRpOnplFnXKX-2o87tcNQ&username=integration_user&password=SecureP@ssw0rd123"
After deploying the Suitelet with "Available Without Login" enabled:
- Go to the Script Deployment record
- Look for the External URL field
- Copy the base URL and append your
usernameandpasswordparameters
Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1Njc4OTAifQ...",
"token_type": "Bearer",
"expires_at": "2024-01-15T10:30:00.000Z"
}
Using the Token to Call NetSuite REST API
# Use the access_token from the response
curl -X POST \
"https://7051733-sb1.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs..." \
-H "Content-Type: application/json" \
-H "Prefer: transient" \
-d '{"q": "SELECT id, companyname FROM customer WHERE rownum <= 10"}'
Complete Example Flow
Authentication Options Comparison
| Method | Pros | Cons |
|---|---|---|
| Username/Password (Current) | Simple, familiar | Credentials in URL (if using params) |
| Headers (X-Username/X-Password) | More secure than URL params | Still plaintext |
| API Key | Single value, simpler | Less granular control |
| Basic Auth | Standard HTTP auth | Requires base64 encoding |
Pass credentials via headers (X-Username, X-Password) instead of URL parameters to avoid credentials appearing in server logs.
Security Enhancements
┌────────────────────────────────────────────────────────────────────────┐
│ TOKEN PROXY SECURITY BEST PRACTICES │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ RECOMMENDED: │
│ ├─► Add API key authentication to Suitelet │
│ ├─► Use IP allowlist if possible │
│ ├─► Log all token requests for audit │
│ ├─► Store private key in File Cabinet (not script) │
│ ├─► Use custom record instead of script parameters │
│ ├─► Add error alerting (email on refresh failure) │
│ └─► Refresh token more frequently than expiry (15 min for 60 min) │
│ │
│ ⚠️ CONSIDERATIONS: │
│ ├─► All clients share one token (audit trail limited) │
│ ├─► Single point of failure (scheduler) │
│ └─► Token exposed if Suitelet URL is discovered │
│ │
└────────────────────────────────────────────────────────────────────────┘
Token Proxy Flow Diagram
What you now have:
- ✅ Scheduled Script deployed
- ✅ Suitelet deployed with external URL
Next: Step 6 - Configure Scheduler Parameters
Step 6: Configure Scheduler Parameters
Now configure the Scheduled Script with all the values collected from previous steps.
6.1: Upload Private Key to File Cabinet
- Go to Documents → Files → File Cabinet
- Navigate to or create a folder:
SuiteScripts/OAuth - Click Add File
- Upload your
private_key.pemfile - Note the File ID (shown in the URL or file list)
┌────────────────────────────────────────────────────────────────────────┐
│ FILE CABINET - PRIVATE KEY │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Folder: SuiteScripts/OAuth │
│ File: private_key.pem │
│ File ID: 12345 ← NOTE THIS! │
│ │
└────────────────────────────────────────────────────────────────────────┘
6.2: Configure Script Parameters
- Go to Customization → Scripting → Scripts
- Find your Scheduled Script (Token Refresher)
- Click Edit or go to Deployments
- Update the script parameters or CONFIG object:
┌────────────────────────────────────────────────────────────────────────┐
│ SCHEDULER CONFIGURATION VALUES │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ CLIENT_ID: [Consumer Key from Step 2] │
│ CERTIFICATE_ID: [Certificate ID from Step 3.4] │
│ ACCOUNT_ID: [Account ID from Step 4] │
│ PRIVATE_KEY_FILE_ID: [File ID from Step 6.1] │
│ TOKEN_RECORD_ID: [Custom Record ID for token storage] │
│ │
└────────────────────────────────────────────────────────────────────────┘
6.3: Update Script CONFIG
Edit the Scheduled Script and update the CONFIG object:
const CONFIG = {
CLIENT_ID: 'a1b2c3d4e5f6...', // From Step 2
CERTIFICATE_ID: '1234567', // From Step 3.4
ACCOUNT_ID: '7051733_SB1', // From Step 4
PRIVATE_KEY_FILE_ID: 12345, // From Step 6.1
TOKEN_RECORD_ID: 1 // Your custom record ID
};
6.4: Verify Custom Record Exists
Ensure the custom record for token storage exists:
- Go to Customization → Lists, Records, & Fields → Record Types
- Find or create
OAuth Tokenrecord - Note the Internal ID of the record instance to store tokens
What you now have:
- ✅ Private key uploaded to File Cabinet
- ✅ Scheduler configured with all parameters:
- Consumer Key (Client ID)
- Certificate ID
- Account ID
- Private Key File ID
- Token Record ID
Next: Step 7 - Test Scheduler Execution
Step 7: Test Scheduler Execution
Run the Scheduled Script manually to verify it can generate a Bearer token.
7.1: Execute Script Manually
- Go to Customization → Scripting → Scripts
- Find your Scheduled Script
- Click View → Deployments
- Click on the deployment
- Click Execute Now (or Save and Execute)
┌────────────────────────────────────────────────────────────────────────┐
│ SCRIPT DEPLOYMENT │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Script: OAuth Token Refresher │
│ Deployment: customdeploy_oauth_refresher │
│ Status: Released │
│ │
│ [Execute Now] [Edit] [View Log] │
│ │
└────────────────────────────────────────────────────────────────────────┘
7.2: Check Execution Log
- After execution, click View Log or go to Customization → Scripting → Script Execution Logs
- Look for your script execution
- Verify success message:
Expected Success Log:
Type: Audit
Title: Token Refresh
Details: Access token refreshed successfully
If Error:
Type: Error
Title: Token Refresh Failed
Details: [Error message - check JWT generation or network issues]
7.3: Verify Token in Custom Record
- Go to your custom record list
- Find the OAuth Token record
- Verify the token was stored:
┌────────────────────────────────────────────────────────────────────────┐
│ OAUTH TOKEN RECORD │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIs... │
│ Expires: 2024-01-15 11:30:00 │
│ Updated: 2024-01-15 10:30:00 │
│ Status: Active │
│ │
└────────────────────────────────────────────────────────────────────────┘
7.4: Test Suitelet Returns Token
-
Open your Suitelet external URL in a browser (with credentials):
https://{ACCOUNT_ID}.extforms.netsuite.com/app/site/hosting/scriptlet.nl
?script=XXX&deploy=X&compid={ACCOUNT_ID}&username=XXX&password=XXX -
Verify JSON response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_at": "2024-01-15T11:30:00.000Z"
}
What you now have:
- ✅ Scheduler executed successfully
- ✅ Bearer token generated and stored
- ✅ Suitelet returns token correctly
Next: Step 8 - Test with Postman
Step 8: Test with Postman
Final validation: Use Postman to get the token from Suitelet and call NetSuite REST API.
8.1: Get Token from Suitelet
Create a new request in Postman:
| Setting | Value |
|---|---|
| Method | GET |
| URL | https://{ACCOUNT_ID}.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=XXX&deploy=X&compid={ACCOUNT_ID} |
Headers:
| Header | Value |
|---|---|
X-Username | integration_user |
X-Password | your_password |
Expected Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQ1Njc4OTAifQ...",
"token_type": "Bearer",
"expires_at": "2024-01-15T11:30:00.000Z"
}
8.2: Call NetSuite REST API with Token
Create another request to test the SuiteQL endpoint:
| Setting | Value |
|---|---|
| Method | POST |
| URL | https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql |
Headers:
| Header | Value |
|---|---|
Authorization | Bearer {access_token from step 8.1} |
Content-Type | application/json |
Prefer | transient |
Body (raw JSON):
{
"q": "SELECT id, companyname FROM customer WHERE rownum <= 5"
}
Expected Response:
{
"links": [...],
"count": 5,
"hasMore": true,
"items": [
{ "id": "1", "companyname": "ABC Company" },
{ "id": "2", "companyname": "XYZ Corp" },
...
],
"offset": 0,
"totalResults": 100
}
8.3: Create Postman Collection (Optional)
Save both requests in a Postman collection for easy reuse:
📁 NetSuite OAuth 2.0
├── 📄 1. Get Token (Suitelet)
└── 📄 2. SuiteQL Query (REST API)
Tip: Use Postman environment variables:
{{account_id}}- Your NetSuite account ID{{access_token}}- Token from Suitelet response{{suitelet_url}}- Your Suitelet external URL
8.4: Troubleshooting
| Error | Cause | Solution |
|---|---|---|
401 Unauthorized | Invalid or expired token | Get fresh token from Suitelet |
403 Forbidden | Role lacks permissions | Check role permissions in NetSuite |
invalid_grant | JWT generation failed | Verify Certificate ID and private key |
Token expired | Scheduler not running | Check scheduler deployment status |
Connection refused | Wrong URL format | Verify Account ID in URL |
What you now have:
- ✅ End-to-end OAuth 2.0 flow working
- ✅ Token generation automated via Scheduler
- ✅ External access via Suitelet
- ✅ REST API calls validated with Postman
Your OAuth 2.0 integration is production ready!
Related Documentation
- Integration Overview - Overview of integration methods
- RESTlet Script Type - Create custom API endpoints
- SuiteQL Reference - Official SuiteQL documentation