Setting up NetSuite token-based authentication (TBA) in 2026, and planning your exit
- Published on
- -12 mins read
- Authors
- Name
- Andy Nur (Andy)
- @andynur
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:
- How TBA works
- Step 1: Enable the features
- Step 2: Create an integration role
- Step 3: Create the integration record
- Step 4: Issue an access token
- Step 5: Sign a request
- Troubleshooting INVALID_LOGIN_ATTEMPT
- The 2027.1 deprecation timeline
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:
| Credential | Comes from | Identifies |
|---|---|---|
| Consumer key / consumer secret | Integration record | The application |
| Token ID / token secret | Access 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 / 71. Create integration record
NetSuite returns a consumer key and consumer secret. They identify the application and are shown only once.
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:
- Enter a name, such as
Shopify Sync, and set State to Enabled. - Under Authentication, check Token-Based Authentication.
- Clear TBA: Authorization Flow and every OAuth 2.0 checkbox unless you need them.
- 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:
- Collect the
oauth_*parameters and any query string parameters. - Percent-encode each key and value (RFC 3986), then sort by key.
- Build the base string:
METHOD&encode(baseUrl)&encode(paramString). The base URL has no query string. - Sign it with HMAC-SHA256, using
encode(consumerSecret)&encode(tokenSecret)as the key. - Send everything in the
Authorizationheader, with the account ID asrealm. 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.
1234567_SB11234567-sb1https://1234567-sb1.suitetalk.api.netsuite.com/services/rest/record/v1https://1234567-sb1.suitetalk.api.netsuite.com/services/rest/query/v1/suiteqlhttps://1234567-sb1.restlets.api.netsuite.com/app/site/hosting/restlet.nlChange 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.timestamp 0Here is the same thing as a small, dependency-free Node module:
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:
import { tbaHeader } from './netsuite-tba.js'
const accountId = process.env.NS_ACCOUNT_ID // e.g. 1234567_SB1const 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 trail | Usual cause |
|---|---|
InvalidSignature | Wrong realm form, query params not signed, or double-encoded values |
InvalidTimestamp | Server clock drift |
UsedNonce | Nonce reused, often by a retry wrapper that resends the same header |
InvalidToken | Token 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.
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.