Setting up NetSuite token-based authentication (TBA) in 2026, and planning your exit

Published on
-
12 mins read
Authors

Token-based authentication (TBA) has been the default way to connect a server to NetSuite for almost a decade. Most iPaaS connectors, Shopify syncs, and custom middleware you will meet in a real account still run on it.

It is also on its way out. From NetSuite 2027.1, you can no longer create new integrations that use TBA. Existing integrations keep working until the final end of support for the TBA feature, which Oracle tentatively plans for 2028.2. So you will maintain TBA integrations for a while, but every new build should use OAuth 2.0.

This guide covers both sides. First, we set up TBA end to end and sign a request by hand, so you understand what every library does for you. Then we look at the timeline and what to do next.

In this article:

Prerequisites

  • Administrator access to a NetSuite sandbox account. Do this in production only after you test it.
  • Node.js 18 or later. The code uses only built-in modules.
  • Basic knowledge of HTTP and the NetSuite REST record API.

How TBA works

TBA is NetSuite's implementation of OAuth 1.0a. There is no login and no token exchange at runtime. You hold four long-lived secrets, and you sign every request with them:

CredentialComes fromIdentifies
Consumer key / consumer secretIntegration recordThe application
Token ID / token secretAccess token (user + role)Who the app acts as

Walk through the flow below. Click an arrow, or press Play.

Anatomy of a TBA request

Step 1 / 7
Adminone-time setupYour serviceNode, Python, iPaaSNetSuiteREST / RESTlet1Create integration record2Create access token3Store 4 secrets4Sign request5GET /record/v1/customer6Verify signature7200 OK / 401

1. Create integration record

NetSuite returns a consumer key and consumer secret. They identify the application and are shown only once.

Click an arrow to jump to that step
TBA has no token endpoint at runtime. Each request carries its own signature.

Step 1: Enable the features

Go to Setup > Company > Enable Features > SuiteCloud and check:

  • Client SuiteScript and Server SuiteScript, if you call RESTlets
  • REST Web Services (and SOAP Web Services only if you still need SOAP)
  • Under Manage Authentication: Token-Based Authentication

Save the page and accept the terms of service if NetSuite asks.

Step 2: Create an integration role

Do not use the Administrator role for integrations. Create a dedicated role at Setup > Users/Roles > Manage Roles > New and give it only what the integration needs.

Required permissions on the Setup subtab:

  • Log in using Access Tokens: Full
  • REST Web Services: Full (for the REST API and SuiteQL)
  • User Access Tokens: Full, only if users must create their own tokens

Then add the record permissions your integration really uses, for example Customers: Edit and Sales Order: Create. Assign the role to a dedicated integration employee record, not to a real person.

Name the employee after the integration

Create an employee such as Integration - Shopify Sync. System notes then show which integration changed a record, and you can turn off one integration without touching a person's login.

Step 3: Create the integration record

Go to Setup > Integration > Manage Integrations > New:

  1. Enter a name, such as Shopify Sync, and set State to Enabled.
  2. Under Authentication, check Token-Based Authentication.
  3. Clear TBA: Authorization Flow and every OAuth 2.0 checkbox unless you need them.
  4. Save.

NetSuite shows the Consumer Key and Consumer Secret once, at the bottom of the confirmation page. Copy them into your secret manager now. If you lose them, you must reset the credentials, which breaks every existing token for that integration.

Step 4: Issue an access token

Go to Setup > Users/Roles > Access Tokens > New, then pick:

  • Application name: the integration record from Step 3
  • User: the integration employee
  • Role: the integration role

Save, then copy the Token ID and Token Secret. You now hold all four secrets.

Step 5: Sign a request

This is where most integrations break. The rules for OAuth 1.0a in NetSuite:

  1. Collect the oauth_* parameters and any query string parameters.
  2. Percent-encode each key and value (RFC 3986), then sort by key.
  3. Build the base string: METHOD&encode(baseUrl)&encode(paramString). The base URL has no query string.
  4. Sign it with HMAC-SHA256, using encode(consumerSecret)&encode(tokenSecret) as the key.
  5. Send everything in the Authorization header, with the account ID as realm. The realm is not part of the signature.

The account ID appears in two forms, and mixing them up is the most common cause of a 401. The realm uses uppercase and an underscore. The hostname uses lowercase and a hyphen.

Account ID → endpoints

Type your account ID the way it shows in the browser address bar or in Company Information.

TBA realm (uppercase, _)1234567_SB1
Hostname ID (lowercase, -)1234567-sb1
REST record APIhttps://1234567-sb1.suitetalk.api.netsuite.com/services/rest/record/v1
SuiteQL endpointhttps://1234567-sb1.suitetalk.api.netsuite.com/services/rest/query/v1/suiteql
RESTlet domainhttps://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nl

Change any field below and watch each stage of the signature update. It runs the same algorithm as the Node code that follows, in your browser, with Web Crypto.

TBA signature playground

Demo values only. Never paste real secrets into a web page.
nonce timestamp 0

Here is the same thing as a small, dependency-free Node module:

netsuite-tba.js
import crypto from 'node:crypto'
const enc = (s) =>
encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
export function tbaHeader({ method, url, accountId, consumerKey, consumerSecret, tokenId, tokenSecret }) {
const u = new URL(url)
const oauth = {
oauth_consumer_key: consumerKey,
oauth_nonce: crypto.randomBytes(16).toString('hex'),
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
oauth_token: tokenId,
oauth_version: '1.0',
}
const params = [...Object.entries(oauth), ...u.searchParams]
.map(([k, v]) => [enc(k), enc(v)])
.sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : 1) : a < b ? -1 : 1))
.map(([k, v]) => `${k}=${v}`)
.join('&')
const base = [method.toUpperCase(), enc(`${u.origin}${u.pathname}`), enc(params)].join('&')
const key = `${enc(consumerSecret)}&${enc(tokenSecret)}`
const signature = crypto.createHmac('sha256', key).update(base).digest('base64')
const realm = accountId.replace(/-/g, '_').toUpperCase()
const fields = Object.entries({ ...oauth, oauth_signature: signature })
.map(([k, v]) => `${k}="${enc(v)}"`)
.join(', ')
return `OAuth realm="${realm}", ${fields}`
}

And a call to the REST record API:

list-customers.js
import { tbaHeader } from './netsuite-tba.js'
const accountId = process.env.NS_ACCOUNT_ID // e.g. 1234567_SB1
const host = accountId.replace(/_/g, '-').toLowerCase()
const url = `https://${host}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=5`
const res = await fetch(url, {
headers: {
Authorization: tbaHeader({
method: 'GET',
url,
accountId,
consumerKey: process.env.NS_CONSUMER_KEY,
consumerSecret: process.env.NS_CONSUMER_SECRET,
tokenId: process.env.NS_TOKEN_ID,
tokenSecret: process.env.NS_TOKEN_SECRET,
}),
},
})
console.log(res.status, await res.json())

Heads up

Generate a new nonce and timestamp for every request, including retries. NetSuite rejects a reused nonce, and it rejects timestamps that are too far from its own clock. If your container clock drifts, every request fails with the same 401.

Troubleshooting INVALID_LOGIN_ATTEMPT

NetSuite returns the same generic error for almost every auth failure. The real reason is in Setup > Users/Roles > View Login Audit Trail. Add the Detail and Role columns to the search.

Detail in the audit trailUsual cause
InvalidSignatureWrong realm form, query params not signed, or double-encoded values
InvalidTimestampServer clock drift
UsedNonceNonce reused, often by a retry wrapper that resends the same header
InvalidTokenToken revoked, the employee or role is inactive, or the integration is disabled

Sandbox refresh

After a sandbox refresh, tokens are copied from production but the integration record may be blocked in the sandbox. Re-enable it, then create fresh tokens for the sandbox.

The 2027.1 deprecation timeline

The dates below come from the NetSuite 2026.1 and 2026.2 release notes. Click each milestone for details.

TBA end of life, release by release

2026.2: Today

TBA still works and you can still create new TBA integrations. This is the window to build new work on OAuth 2.0 and to inventory existing TBA integration records.

Shipped Current release Upcoming

What this means for you in practice:

  • New integration? Use OAuth 2.0 client credentials (M2M). It is the direct replacement for server-to-server TBA.
  • Existing TBA integration? It does not stop on 2027.1, but it has a tentative end date in 2028.2. Inventory your integration records now, and migrate them one by one, starting with the ones you change most often.
  • Still on NLAuth (email and password in the header)? That stops working in 2027.1. Move it first.
  • Connecting an AI assistant? Skip both. The NetSuite AI Connector Service handles OAuth 2.0 for you.

Conclusion

TBA is simple once you see the pieces: four secrets, one signature per request, and a realm in the header. Most failures come from the signature base string or the account ID format, and the Login Audit Trail tells you which one.

Use this knowledge to maintain and debug the TBA integrations you already have. For anything new in 2026, go straight to OAuth 2.0.