This card covers authentication methods for Oracle NetSuite's API surfaces. NetSuite supports two primary programmatic authentication protocols: Token-Based Authentication (TBA, built on OAuth 1.0a) and OAuth 2.0. The choice between them depends on the API surface, integration pattern, and timeline — TBA is being phased out in favor of OAuth 2.0 across all API surfaces except SOAP (which itself is being deprecated). [src1, src2]
| Property | Value |
|---|---|
| Vendor | Oracle |
| System | Oracle NetSuite 2026.1 |
| API Surface | REST Web Services, RESTlets, SOAP Web Services, SuiteAnalytics Connect |
| Editions Covered | All NetSuite editions |
| Deployment | Cloud |
| API Docs | NetSuite Help Center — Authentication |
| Status | OAuth 2.0: GA (preferred); TBA: GA (legacy, sunset in progress) |
Not all API surfaces support both authentication methods. This is the single most important table for deciding which auth method to use. [src1, src2, src6]
| API Surface | TBA (OAuth 1.0a) | OAuth 2.0 Auth Code | OAuth 2.0 M2M | Notes |
|---|---|---|---|---|
| REST Web Services | Yes | Yes | Yes | OAuth 2.0 preferred for all new integrations |
| RESTlets | Yes | Yes | Yes | OAuth 2.0 preferred; user credentials deprecated since 2021 |
| SOAP Web Services | Yes | No | No | TBA is the ONLY programmatic auth for SOAP |
| SuiteAnalytics Connect | Yes | Yes | Yes | OAuth 2.0 resolves 2FA incompatibility issues |
| SuiteCloud SDK (SDF) | No (removed 2024.2) | Yes | N/A | SDK 24.2+ requires OAuth 2.0 |
Authentication method does not directly change API rate limits — limits are governed by the API surface and account tier. However, token lifetime and refresh behavior differ significantly. [src1, src2]
| Property | TBA (OAuth 1.0a) | OAuth 2.0 Auth Code | OAuth 2.0 M2M |
|---|---|---|---|
| Access token lifetime | Never expires (until revoked) | 60 min (configurable) | 60 min |
| Refresh token | N/A — permanent | Yes — valid until revoked | No — re-authenticate via JWT |
| Request signing | HMAC-SHA256 per request | Bearer token | Bearer token |
| Certificate required | No | No | Yes — X.509 RSA 3072/4096 bit |
| Auth Method | Overhead per Request | Notes |
|---|---|---|
| TBA | ~1-3ms (signature computation) | HMAC-SHA256 computed client-side for every request |
| OAuth 2.0 | ~0ms (bearer token attach) | Token cached; no per-request computation |
| OAuth 2.0 M2M (token fetch) | ~200-500ms (JWT signing + HTTP roundtrip) | Only when token expires (every 60 min) |
| Flow | Protocol | Use When | Token Lifetime | Refresh? | SOAP? | Setup Complexity |
|---|---|---|---|---|---|---|
| TBA | OAuth 1.0a | Legacy SOAP; existing integrations not yet migrated | Permanent | N/A | Yes | Medium |
| Authorization Code | OAuth 2.0 | User-context apps (portals, interactive) | 60 min | Yes | No | Medium |
| Client Credentials (M2M) | OAuth 2.0 | Server-to-server, unattended, middleware | 60 min | No | No | High |
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 730. Keep key.pem private. [src3]https://TSTDRV1234567.suitetalk.api.netsuite.com). Getting this wrong is the #1 auth failure. [src1]START -- New NetSuite integration, choosing auth method
|
+-- Which API surface?
| |
| +-- SOAP Web Services?
| | +-- YES --> TBA is required (OAuth 2.0 not supported for SOAP)
| | | +-- Is this a new integration?
| | | +-- YES --> STOP. Do NOT build new SOAP integrations.
| | | | Migrate to REST + OAuth 2.0 instead. SOAP sunsets 2028.2.
| | | +-- NO (existing) --> Keep TBA, plan REST migration before 2028.2
| | +-- NO --> Continue to next question
| |
| +-- REST Web Services or RESTlets?
| | +-- Is this server-to-server (no user context)?
| | | +-- YES --> OAuth 2.0 Client Credentials (M2M)
| | | | Requirements: X.509 cert, M2M mapping per account
| | | +-- NO --> OAuth 2.0 Authorization Code
| | | | Requirements: Redirect URI, user consent flow
| |
| +-- SuiteAnalytics Connect (ODBC/JDBC)?
| | +-- OAuth 2.0 (resolves 2FA issues with legacy passwords)
| |
| +-- SuiteCloud SDK / SDF?
| +-- OAuth 2.0 (mandatory since SDK 24.2)
|
+-- Migrating existing TBA integration?
+-- REST/RESTlet? --> Switch to OAuth 2.0 M2M or Auth Code
+-- SOAP? --> Migrate to REST first, then OAuth 2.0
+-- Timeline: Complete before 2027.1 (new TBA blocked)
| Capability | TBA (OAuth 1.0a) | OAuth 2.0 |
|---|---|---|
| Protocol standard | OAuth 1.0a | OAuth 2.0 (RFC 6749) |
| Request signing | HMAC-SHA256 per request | None (bearer token) |
| Token expiration | Never (until revoked) | 60 minutes (access token) |
| Refresh mechanism | N/A | Refresh token (auth code) or re-authenticate (M2M) |
| SOAP support | Yes | No |
| REST support | Yes | Yes |
| RESTlet support | Yes | Yes |
| SuiteAnalytics Connect | Yes | Yes |
| SuiteCloud SDK | No (removed 2024.2) | Yes (required) |
| Certificate required | No | Only for M2M (X.509 RSA 3072/4096) |
| Implementation complexity | Medium (signature logic) | Low (auth code) / High (M2M cert mgmt) |
| Security model | Shared secrets (4 values) | Short-lived tokens, certificate-based (M2M) |
| Status (2026) | Legacy — sunset in progress | Preferred — actively developed |
| New integrations after 2027.1 | Blocked | Required |
| Date/Release | Event | Impact |
|---|---|---|
| 2024.2 (Aug 2024) | TBA removed from SuiteCloud SDK | SDK users must switch to OAuth 2.0 |
| 2025.2 | Last planned SOAP endpoint released | No new SOAP versions after this |
| 2026.1 | Oracle recommends REST + OAuth 2.0 for all new integrations | Soft recommendation, not enforced |
| 2027.1 | No new TBA integrations for SOAP, REST, or RESTlets | Hard enforcement — existing TBA continues |
| 2027.2 | Only 2025.2 SOAP endpoint supported; older unsupported | Older SOAP endpoints still operational |
| 2028.2 | All SOAP endpoints disabled | SOAP integrations stop working entirely |
Use the decision tree above. For new server-to-server integrations, OAuth 2.0 M2M is the recommended path. [src1]
Verify: Check Setup > Integration > Manage Integrations to confirm OAuth 2.0 and REST Web Services are enabled.
Generate an RSA 4096-bit X.509 certificate and configure the M2M mapping. [src3]
# Generate RSA 4096-bit key pair and self-signed X.509 certificate (valid 2 years)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 730 \
-subj "/CN=netsuite-integration/O=YourCompany"
Verify: openssl x509 -in cert.pem -text -noout | grep "Public-Key" → expected: Public-Key: (4096 bit)
In NetSuite UI, create the integration record with M2M grant and map entity/role/certificate. [src3]
Verify: The M2M Setup page shows your mapping as "Active".
Build a JWT, sign it with your private key, and exchange it at the token endpoint. [src3]
# Input: client_id, certificate_id, account_id, private key (key.pem)
# Output: access_token (valid 60 minutes)
import jwt
import time
import requests
ACCOUNT_ID = "TSTDRV1234567"
CLIENT_ID = "your-client-id-from-integration-record"
CERTIFICATE_ID = "your-certificate-id-from-m2m-setup"
TOKEN_URL = f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token"
with open("key.pem", "r") as f:
private_key = f.read()
now = int(time.time())
payload = {
"iss": CLIENT_ID,
"scope": "rest_webservices",
"aud": TOKEN_URL,
"exp": now + 300,
"iat": now,
}
assertion = jwt.encode(payload, private_key, algorithm="RS256",
headers={"kid": CERTIFICATE_ID})
resp = requests.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": assertion,
})
resp.raise_for_status()
access_token = resp.json()["access_token"]
Verify: Response contains access_token and expires_in: 3600.
Use the bearer token to call NetSuite REST Web Services. [src1]
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
response = requests.get(
f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/customer/123",
headers=headers
)
print(response.json())
Verify: HTTP 200 response with customer record JSON.
# Input: consumer_key, consumer_secret, token_id, token_secret, account_id
# Output: Authenticated REST API call using TBA
import requests
from requests_oauthlib import OAuth1
ACCOUNT_ID = "TSTDRV1234567"
BASE_URL = f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1"
auth = OAuth1(
client_key="your-consumer-key",
client_secret="your-consumer-secret",
resource_owner_key="your-token-id",
resource_owner_secret="your-token-secret",
realm=ACCOUNT_ID,
signature_method="HMAC-SHA256"
)
response = requests.get(f"{BASE_URL}/customer/123", auth=auth)
print(response.status_code, response.json())
// Input: clientId, certificateId, privateKey (PEM), accountId
// Output: access_token string
const jwt = require("jsonwebtoken"); // v9.x
const axios = require("axios"); // v1.x
const fs = require("fs");
const ACCOUNT_ID = "TSTDRV1234567";
const CLIENT_ID = "your-client-id";
const CERT_ID = "your-certificate-id";
const TOKEN_URL = `https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`;
const privateKey = fs.readFileSync("key.pem", "utf8");
const assertion = jwt.sign(
{ iss: CLIENT_ID, scope: "rest_webservices", aud: TOKEN_URL,
exp: Math.floor(Date.now() / 1000) + 300,
iat: Math.floor(Date.now() / 1000) },
privateKey,
{ algorithm: "RS256", header: { kid: CERT_ID } }
);
const params = new URLSearchParams({
grant_type: "client_credentials",
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion: assertion,
});
const response = await axios.post(TOKEN_URL, params);
console.log("Token:", response.data.access_token);
# Input: JWT assertion (pre-built), account ID
# Output: JSON with access_token, token_type, expires_in
curl -X POST \
"https://TSTDRV1234567.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
-d "client_assertion=YOUR_JWT_ASSERTION_HERE"
# Expected: { "access_token": "...", "token_type": "Bearer", "expires_in": 3600 }
| TBA Credential | OAuth 2.0 Equivalent | Where to Find | Migration Notes |
|---|---|---|---|
| Consumer Key | Client ID | Integration record | Same field, different label in UI |
| Consumer Secret | Client Secret (auth code) / N/A (M2M) | Integration record | M2M uses certificate, not secret |
| Token ID | Access Token (auto-generated) | Token endpoint response | OAuth 2.0 tokens are ephemeral |
| Token Secret | N/A | N/A | OAuth 2.0 uses bearer tokens, no per-request secrets |
| N/A | Certificate ID | M2M setup page | New concept — not present in TBA |
| N/A | Refresh Token | Token endpoint response | Only in authorization code flow |
realm=ACCOUNT_ID in OAuth header. OAuth 2.0 encodes account in the URL instead. Remove realm logic when migrating. [src2]| Error | Auth Method | Cause | Resolution |
|---|---|---|---|
invalid_grant | OAuth 2.0 M2M | JWT expired, wrong audience, or certificate not mapped | Verify JWT exp/aud claims; confirm M2M mapping in this account |
invalid_client | OAuth 2.0 | Client ID not found or integration disabled | Check integration record is active; verify Client ID |
unauthorized (401) | TBA | Invalid signature — wrong secret, timestamp skew, nonce reuse | Verify all 4 credentials; check clock sync (<5min skew) |
INVALID_LOGIN_ATTEMPT | Both | Role lacks "Log in using Access Tokens" permission | Add permission to the role |
SSS_REQUEST_LIMIT_EXCEEDED | Both | Governance limit hit | Reduce frequency; implement backoff |
INSUFFICIENT_PERMISSION | Both | Role lacks required record/field permissions | Audit role permissions |
invalid_grant. Fix: Monitor cert expiration; rotate at least 30 days before expiry. [src3]Automate M2M recreation as part of sandbox refresh runbook. [src3]Use NTP; include clock sync in health monitoring. [src2]Single-writer refresh with distributed lock. [src5]Monitor integration record status; alert on changes. [src6]# BAD -- tokens in code, impossible to rotate without redeployment
auth = OAuth1(
client_key="abc123",
client_secret="secret456",
resource_owner_key="tok789",
resource_owner_secret="toksecret012",
realm="TSTDRV1234567",
signature_method="HMAC-SHA256"
)
# GOOD -- credentials externalized, rotatable without code changes
import os
auth = OAuth1(
client_key=os.environ["NS_CONSUMER_KEY"],
client_secret=os.environ["NS_CONSUMER_SECRET"],
resource_owner_key=os.environ["NS_TOKEN_ID"],
resource_owner_secret=os.environ["NS_TOKEN_SECRET"],
realm=os.environ["NS_ACCOUNT_ID"],
signature_method="HMAC-SHA256"
)
# BAD -- SOAP sunset 2028.2. New SOAP blocked after 2027.1.
from zeep import Client
wsdl = f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/wsdl/v2025_2_0/netsuite.wsdl"
client = Client(wsdl)
# GOOD -- REST is the future. OAuth 2.0 is the preferred auth.
import requests
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(
f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/record/v1/salesOrder",
headers=headers,
params={"q": "status IS SalesOrd:B"}
)
# BAD -- unnecessary token requests; wastes time and may hit rate limits
def make_api_call(endpoint):
token = get_new_token() # 200-500ms overhead EVERY call
return requests.get(endpoint, headers={"Authorization": f"Bearer {token}"})
# GOOD -- cache token, refresh only when expired
import time
_token_cache = {"token": None, "expires_at": 0}
def get_token():
if time.time() < _token_cache["expires_at"] - 60: # 60s buffer
return _token_cache["token"]
resp = request_new_token()
_token_cache["token"] = resp["access_token"]
_token_cache["expires_at"] = time.time() + resp["expires_in"]
return _token_cache["token"]
Include M2M setup in sandbox refresh checklist. [src3]Always specify -newkey rsa:4096. [src3]Audit integration provisioning scripts; switch to OAuth 2.0 before 2027.1. [src2]Ensure NTP is running; add clock sync monitoring. [src2, src6]Set calendar alerts; use Certificate Rotation Endpoint. [src3]# Test OAuth 2.0 M2M token request
curl -s -X POST \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token" \
-d "grant_type=client_credentials&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=JWT_HERE" \
| python3 -m json.tool
# Check certificate expiration date
openssl x509 -in cert.pem -noout -enddate
# Check certificate key length
openssl x509 -in cert.pem -noout -text | grep "Public-Key"
# Test REST API with bearer token
curl -s -X GET \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1" \
-H "Authorization: Bearer ACCESS_TOKEN" \
| python3 -m json.tool
# Check integration record status (via SuiteQL)
curl -s -X POST \
"https://ACCOUNT_ID.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Prefer: transient" \
-d '{"q": "SELECT id, name, isinactive FROM integration WHERE isinactive = '\''F'\'' ORDER BY id"}'
| Feature/Release | Date | Status | Breaking Changes | Migration Notes |
|---|---|---|---|---|
| TBA introduced | 2015.2 | GA | N/A | First programmatic auth beyond user credentials |
| HMAC-SHA1 deprecated for TBA | 2021.1 | Enforced | HMAC-SHA1 signatures rejected | Switch to HMAC-SHA256 |
| User credentials deprecated (RESTlets) | 2021.x | Enforced | Username/password auth removed | Use TBA or OAuth 2.0 |
| OAuth 2.0 Authorization Code GA | 2021.2 | GA | N/A | New option for user-context integrations |
| OAuth 2.0 Client Credentials (M2M) GA | 2022.2 | GA | N/A | Server-to-server without user context |
| TBA removed from SuiteCloud SDK | 2024.2 | Enforced | SDK no longer accepts TBA | Use OAuth 2.0 for SDF |
| 2025.2 — last SOAP endpoint | 2025 H2 | Current | N/A | Final SOAP version with planned support |
| 2027.1 — no new TBA | 2027 H1 | Upcoming | Cannot create new TBA integration records | Migrate existing before this |
| 2028.2 — SOAP disabled | 2028 H2 | Upcoming | All SOAP endpoints turned off | Must be on REST + OAuth 2.0 |
Oracle NetSuite supports each SOAP endpoint for 3 years from release. After 3 years, the endpoint becomes "unsupported but available" and is eventually disabled. OAuth 2.0 is the long-term standard; TBA blocked for new integrations at 2027.1; SOAP fully disabled at 2028.2. [src4]
| Use OAuth 2.0 When | Use TBA When | Notes |
|---|---|---|
| New server-to-server integration (M2M) | Integrating with SOAP web services | SOAP is the only API surface that requires TBA |
| New user-context app (portal, interactive) | You need permanent non-expiring tokens | Plan migration anyway — TBA is legacy |
| SuiteCloud SDK / SDF development | Building against SOAP-only features with no REST equivalent | Consider RESTlet wrapper + OAuth 2.0 instead |
| Need short-lived, rotatable credentials | Legacy middleware only supports OAuth 1.0a | Update middleware when possible |
| Migrating from TBA and timeline allows | Integration must work across sandbox without re-setup | M2M mapping is per-account for OAuth 2.0 |