Skip to main content

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:

FlowUse CaseDescription
Client CredentialsMachine-to-MachineServer-to-server integration without user interaction
Authorization CodeUser ContextUser 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.

StepActionResultUsed In
1Enable OAuth 2.0 Feature✅ OAuth 2.0 & REST Web Services enabledStep 2 (allows integration creation)
2Create Integration RecordConsumer Key (Client ID)Step 6 (Scheduler config)
Consumer SecretStore securely (backup only)
3.1Generate RSA Keypairprivate_key.pemStep 6 (upload to File Cabinet)
public_cert.pemStep 3.3 (upload to NetSuite)
3.2Verify Certificate & Key✅ Keys validated-
3.3Upload Certificate to NetSuite✅ Certificate registered-
3.4Note Certificate IDCertificate IDStep 6 (Scheduler config)
4Find NetSuite URLsAccount IDStep 6 (Scheduler config)
Token Endpoint URLStep 6 (token request)
REST API Base URLStep 8 (API calls)
5Deploy Token Proxy ScriptsScheduler + Suitelet deployedStep 6
6Configure Scheduler Parameters✅ All config values setStep 7
7Test Scheduler Execution✅ Bearer token generatedStep 8
8Test with Postman✅ API call successfulProduction 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​

StepActionOutput
1Enable OAuth 2.0 FeatureFeature enabled
2Create Integration RecordConsumer Key
3Create Certificateprivate_key.pem, Certificate ID
4Get NetSuite URLsAccount ID
5Deploy ScriptsScheduler + Suitelet
6Configure SchedulerAdd all values to CONFIG
7Test SchedulerBearer Token Generated
8Test with PostmanAPI Call Success

Flow: Step 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8

Dependencies to Step 6: Consumer Key (from 2), private_key.pem + Certificate ID (from 3), Account ID (from 4)


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:​

  1. Go to Setup → Company → Enable Features
  2. Click on the SuiteCloud subtab
  3. Under SuiteScript, ensure these are checked:
    • ☑ Client SuiteScript
    • ☑ Server SuiteScript
  4. Under Manage Authentication, check:
    • ☑ OAuth 2.0
  5. Under SuiteTalk (Web Services), check:
    • ☑ REST Web Services
  6. Click Save
Important

You must have the Administrator role to enable these features. Changes take effect immediately.

Step 1 Result

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​

FieldValueNotes
NameMy OAuth2 IntegrationDescriptive name
StateEnabledMust be enabled
OAuth 2.0☑ CheckedEnable OAuth 2.0 for this integration
Token-Based Authentication☐ UncheckedNot needed for OAuth 2.0
Authorization Code Grant☑ or ☐Check if using Authorization Code flow
Client Credentials (M2M)☑ CheckedRequired for machine-to-machine
REST Web Services☑ CheckedEnable REST API access

Detailed Steps:​

  1. Go to Setup → Integration → Manage Integrations
  2. Click New
  3. 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 │
│ │
└────────────────────────────────────────────────────────────────────────┘
  1. 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. │
│ │
└────────────────────────────────────────────────────────────────────────┘
Critical

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.

Step 2 Result

What you now have:

  • Consumer Key (Client ID) → Used in Step 3.3 and Step 5 (JWT iss claim)
  • 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
Windows Users

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
  1. Go to Setup → Integration → OAuth 2.0 Client Credentials (M2M) Setup
  2. Click Create New
  3. 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----- │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
  1. Either:
    • Click Choose File and select your public_cert.pem, OR
    • Copy/paste the contents of public_cert.pem into the text area
  2. 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 │
│ │
└────────────────────────────────────────────────────────────────────────┘
Step 3 Result

What you now have:

  • private_key.pem → Used in Step 6 (upload to File Cabinet)
  • public_cert.pem → Already uploaded to NetSuite
  • Certificate ID → Used in Step 6 (Scheduler config)

Collected so far:

ValueFrom StepUsed In
Consumer Key (Client ID)Step 2Step 6 (Scheduler config)
private_key.pemStep 3.1Step 6 (File Cabinet)
Certificate IDStep 3.4Step 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:

EnvironmentAccount ID FormatExample
ProductionNumeric or with suffix1234567 or 1234567_SB1
SandboxWith _SB# suffix1234567_SB1, 1234567_SB2

To find your Account ID:

  1. Go to Setup → Company → Company Information
  2. Look for Account ID field

Or check your NetSuite URL:

https://1234567.app.netsuite.com/...
↑↑↑↑↑↑↑
Account ID
Step 4 Result

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:

ValueFrom StepUsed In Step 6
Consumer Key (Client ID)Step 2Scheduler CONFIG
private_key.pemStep 3.1Upload to File Cabinet
Certificate IDStep 3.4Scheduler CONFIG
Account IDStep 4Scheduler 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​

NetSuite:

  1. Scheduled Script (every 15 min) → Generate JWT → Request Access Token → Store in Custom Record

External System:

  1. External App → GET with API Key → Suitelet → Returns Access Token
  2. External App → Use Token → Call NetSuite REST API

When to Use This Pattern​

Use CaseRecommended?
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​

Create a custom record to store the token:

FieldTypeDescription
custrecord_oauth_tokenText AreaThe access token
custrecord_oauth_expiresDate/TimeToken expiration timestamp
custrecord_oauth_updatedDate/TimeLast refresh time
custrecord_oauth_statusListActive/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} │
│ │
└────────────────────────────────────────────────────────────────────────┘
ParameterDescriptionExample
{ACCOUNT_ID}NetSuite account ID7051733_SB1
scriptScript internal ID350
deployDeployment ID1
compidCompany/Account ID7051733_SB1
ns-atNetSuite auth token (auto-generated)AAEJ7tMQ...
usernameYour integration usernameintegration_user
passwordYour integration passwordSecureP@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"
Finding the External URL

After deploying the Suitelet with "Available Without Login" enabled:

  1. Go to the Script Deployment record
  2. Look for the External URL field
  3. Copy the base URL and append your username and password parameters

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​

MethodProsCons
Username/Password (Current)Simple, familiarCredentials in URL (if using params)
Headers (X-Username/X-Password)More secure than URL paramsStill plaintext
API KeySingle value, simplerLess granular control
Basic AuthStandard HTTP authRequires base64 encoding
Recommended: Use Headers

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​

Step 5 Result

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​

  1. Go to Documents → Files → File Cabinet
  2. Navigate to or create a folder: SuiteScripts/OAuth
  3. Click Add File
  4. Upload your private_key.pem file
  5. 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​

  1. Go to Customization → Scripting → Scripts
  2. Find your Scheduled Script (Token Refresher)
  3. Click Edit or go to Deployments
  4. 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:

  1. Go to Customization → Lists, Records, & Fields → Record Types
  2. Find or create OAuth Token record
  3. Note the Internal ID of the record instance to store tokens
Step 6 Result

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​

  1. Go to Customization → Scripting → Scripts
  2. Find your Scheduled Script
  3. Click View → Deployments
  4. Click on the deployment
  5. 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​

  1. After execution, click View Log or go to Customization → Scripting → Script Execution Logs
  2. Look for your script execution
  3. 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​

  1. Go to your custom record list
  2. Find the OAuth Token record
  3. 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​

  1. 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
  2. Verify JSON response:

    {
    "access_token": "eyJhbGciOiJSUzI1NiIs...",
    "token_type": "Bearer",
    "expires_at": "2024-01-15T11:30:00.000Z"
    }
Step 7 Result

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:

SettingValue
MethodGET
URLhttps://{ACCOUNT_ID}.extforms.netsuite.com/app/site/hosting/scriptlet.nl?script=XXX&deploy=X&compid={ACCOUNT_ID}

Headers:

HeaderValue
X-Usernameintegration_user
X-Passwordyour_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:

SettingValue
MethodPOST
URLhttps://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql

Headers:

HeaderValue
AuthorizationBearer {access_token from step 8.1}
Content-Typeapplication/json
Prefertransient

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​

ErrorCauseSolution
401 UnauthorizedInvalid or expired tokenGet fresh token from Suitelet
403 ForbiddenRole lacks permissionsCheck role permissions in NetSuite
invalid_grantJWT generation failedVerify Certificate ID and private key
Token expiredScheduler not runningCheck scheduler deployment status
Connection refusedWrong URL formatVerify Account ID in URL
Step 8 Result - Setup Complete!

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!