# NetSuite Token-Based Authentication (OAuth 1.0) Signature Code Generator

> Source: https://netray.co/tools/netsuite-tba-oauth1-signature-code-generator
> Publisher: Netray (netray.co), AI and integration engineering for ERP and manufacturing systems.
> Section: Free Tools

## Short answer

Get dependency-free Python and Node.js code that builds a valid NetSuite TBA Authorization header for a RESTlet or REST web services call, then sends it.

## Category

developer

## Erp

NetSuite

## Audience

- NetSuite developer
- Integration developer
- NetSuite administrator

## Tool

**Kind:** template

**Output:** code

**Language:** python

**Fields:**

#### Account ID

**Key:** account_id

**Type:** text

**Default:** 1234567

**Help:** Production 1234567, sandbox 1234567_SB1 or 1234567-sb1. The code builds the realm as uppercase with an underscore.

#### HTTP method

**Key:** http_method

**Type:** select

**Options:**

- GET
- POST
- PUT
- DELETE

**Default:** POST

#### Full request URL

**Key:** url

**Type:** text

**Default:** https://1234567.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script=123&deploy=1

**Help:** Copy the External URL from the script deployment. The hostname uses lowercase and a hyphen for sandboxes.

#### Consumer key (integration record)

**Key:** consumer_key

**Type:** text

**Default:** <consumer_key>

#### Consumer secret

**Key:** consumer_secret

**Type:** text

**Default:** <consumer_secret>

#### Token ID (access token)

**Key:** token_id

**Type:** text

**Default:** <token_id>

#### Token secret

**Key:** token_secret

**Type:** text

**Default:** <token_secret>

**Blocks:**

#### 1. Python 3 (standard library only)

**Body:** import base64
import hashlib
import hmac
import json
import secrets
import time
import urllib.parse
import urllib.request

ACCOUNT_ID = "{{account_id}}"
HTTP_METHOD = "{{http_method}}"
URL = "{{url}}"
CONSUMER_KEY = "{{consumer_key}}"
CONSUMER_SECRET = "{{consumer_secret}}"
TOKEN_ID = "{{token_id}}"
TOKEN_SECRET = "{{token_secret}}"

def enc(value):
    # RFC 3986 percent encoding (same as PHP rawurlencode)
    return urllib.parse.quote(str(value), safe="~")

def tba_header(method, url):
    realm = ACCOUNT_ID.upper().replace("-", "_")  # 1234567_SB1 style
    parts = urllib.parse.urlsplit(url)
    base_url = parts.scheme.lower() + "://" + parts.netloc.lower() + parts.path
    oauth = {
        "oauth_consumer_key": CONSUMER_KEY,
        "oauth_token": TOKEN_ID,
        "oauth_signature_method": "HMAC-SHA256",
        "oauth_timestamp": str(int(time.time())),
        "oauth_nonce": secrets.token_hex(16),
        "oauth_version": "1.0",
    }
    params = list(oauth.items()) + urllib.parse.parse_qsl(parts.query, keep_blank_values=True)
    normalized = "&".join(k + "=" + v for k, v in sorted((enc(k), enc(v)) for k, v in params))
    base_string = method.upper() + "&" + enc(base_url) + "&" + enc(normalized)
    key = enc(CONSUMER_SECRET) + "&" + enc(TOKEN_SECRET)
    digest = hmac.new(key.encode(), base_string.encode(), hashlib.sha256).digest()
    oauth["oauth_signature"] = base64.b64encode(digest).decode()
    fields = ["realm=\"" + realm + "\""] + [k + "=\"" + enc(v) + "\"" for k, v in oauth.items()]
    return "OAuth " + ", ".join(fields)

if __name__ == "__main__":
    header = tba_header(HTTP_METHOD, URL)
    print("Authorization:", header)
    data = json.dumps({"ping": True}).encode() if HTTP_METHOD in ("POST", "PUT") else None
    req = urllib.request.Request(URL, data=data, method=HTTP_METHOD,
                                 headers={"Authorization": header, "Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as resp:
        print(resp.status, resp.read().decode())

#### 2. Node.js (built-in crypto and fetch)

**Body:** // Node.js 18 or later, no packages needed
const crypto = require('crypto');

const ACCOUNT_ID = '{{account_id}}';
const HTTP_METHOD = '{{http_method}}';
const URL_STRING = '{{url}}';
const CONSUMER_KEY = '{{consumer_key}}';
const CONSUMER_SECRET = '{{consumer_secret}}';
const TOKEN_ID = '{{token_id}}';
const TOKEN_SECRET = '{{token_secret}}';

const enc = (value) => encodeURIComponent(String(value))
  .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());

function tbaHeader(method, urlString) {
  const realm = ACCOUNT_ID.toUpperCase().replace(/-/g, '_');
  const url = new URL(urlString);
  const baseUrl = url.protocol + '//' + url.host + url.pathname;
  const oauth = {
    oauth_consumer_key: CONSUMER_KEY,
    oauth_token: TOKEN_ID,
    oauth_signature_method: 'HMAC-SHA256',
    oauth_timestamp: Math.floor(Date.now() / 1000).toString(),
    oauth_nonce: crypto.randomBytes(16).toString('hex'),
    oauth_version: '1.0',
  };
  const pairs = Object.entries(oauth).concat(Array.from(url.searchParams.entries()))
    .map(([k, v]) => [enc(k), enc(v)])
    .sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : 1) : (a[0] < b[0] ? -1 : 1)));
  const normalized = pairs.map(([k, v]) => k + '=' + v).join('&');
  const baseString = method.toUpperCase() + '&' + enc(baseUrl) + '&' + enc(normalized);
  const key = enc(CONSUMER_SECRET) + '&' + enc(TOKEN_SECRET);
  oauth.oauth_signature = crypto.createHmac('sha256', key).update(baseString).digest('base64');
  const fields = ['realm="' + realm + '"'].concat(
    Object.entries(oauth).map(([k, v]) => k + '="' + enc(v) + '"'));
  return 'OAuth ' + fields.join(', ');
}

(async () => {
  const header = tbaHeader(HTTP_METHOD, URL_STRING);
  const options = { method: HTTP_METHOD, headers: { Authorization: header, 'Content-Type': 'application/json' } };
  if (HTTP_METHOD === 'POST' || HTTP_METHOD === 'PUT') options.body = JSON.stringify({ ping: true });
  const res = await fetch(URL_STRING, options);
  console.log(res.status, await res.text());
})();

**Notes:** Never commit real secrets: load them from environment variables or a vault in production. HMAC-SHA1 support ended in 2023.1, so the method is fixed at HMAC-SHA256. Query string parameters (script, deploy and any others) are part of the signature; a JSON body is not. Oracle has announced that from 2027.1 no new TBA integrations can be created for SOAP, REST web services or RESTlets; existing ones keep working, and new work should use OAuth 2.0.

## Content

**Intro:** This generator writes Python and Node.js code that signs a NetSuite RESTlet or REST web services request with token-based authentication (OAuth 1.0a, HMAC-SHA256) and sends it. It is for developers debugging INVALID_LOGIN_ATTEMPT errors or replacing a Postman setup with a script.

**Sections:**

#### How to use it

**Body:** Create an integration record with Token-Based Authentication checked (Setup > Integration > Manage Integrations) and copy the consumer key and secret, which are shown only once. Then create an access token for the user and role (Setup > Users/Roles > Access Tokens) and copy the token ID and secret. Paste all four, the account ID and the RESTlet External URL from the script deployment, choose the method, and run either block. The script prints the header and the response, so you can compare the header with what Postman generates when something fails.

**Bullets:**

- The role needs the User Access Tokens and Log in using Access Tokens permissions.
- For REST web services also grant REST Web Services permission.
- Signatures expire: timestamps must be close to NetSuite server time.

#### How the signature is built

**Body:** The code follows the OAuth 1.0a rules Oracle documents for RESTlets and REST web services. It collects oauth_consumer_key, oauth_token, oauth_signature_method (HMAC-SHA256), oauth_timestamp, oauth_nonce and oauth_version (1.0), adds every query string parameter from the URL, percent-encodes each name and value per RFC 3986, sorts them and joins them with &. The base string is the uppercase method, the encoded base URL without the query string, and the encoded parameter string, joined by &. The key is the encoded consumer secret and token secret joined by &. The signature is the Base64 of the HMAC-SHA256 digest. The realm goes in the header but not in the base string.

#### Version differences and gotchas

**Body:** The realm and the hostname spell sandbox accounts differently: the realm is uppercase with an underscore (1234567_SB1) while the hostname is lowercase with a hyphen (1234567-sb1.restlets.api.netsuite.com). Mixing them up gives INVALID_LOGIN_ATTEMPT with no further detail, and the Login Audit Trail then shows the reason. Other common causes: forgetting script and deploy in the signed parameters, a nonce reused within the timestamp window, a clock that is minutes off, and encoding with a form encoder that turns spaces into + instead of %20. HMAC-SHA1 no longer works after 2023.1. Because the header covers the query string, every distinct URL, including each paging offset, needs its own signature.

#### Checking the result in NetSuite

**Body:** When a call fails, open Setup > Users/Roles > View Login Audit Trail, filter on the user, and add the Detail and Token-based Access Token Name columns. The detail column says whether the signature, nonce, timestamp, token or role was the problem. A 403 with a valid login usually means the role lacks permission for what the RESTlet does, not an authentication fault. For RESTlets, the script deployment must be Released with the calling role in the audience; a deployment in Testing runs only for the script owner. Check the RESTlet execution log for errors once authentication passes.

**Cta:** If you want integrations and an assistant that read NetSuite under the right role with keys kept on your own servers, we can set that up.

**Faq:**

#### How do I authenticate to a NetSuite RESTlet?

**Answer:** Use OAuth 2.0 for new work, or token-based authentication on existing integrations: an Authorization: OAuth header with realm, consumer key, token, nonce, timestamp, HMAC-SHA256 signature method, version 1.0 and the signature. The signature covers the method, the URL without query and all oauth and query parameters, including script and deploy.

#### Where do I get a NetSuite API key for TBA?

**Answer:** NetSuite has no single API key. The consumer key and secret come from an integration record with Token-Based Authentication enabled, shown once at save. The token ID and secret come from an access token created for a specific user, role and integration. Together the four values sign each request.

#### Why do I get INVALID_LOGIN_ATTEMPT with TBA?

**Answer:** Most often the signature does not match: wrong realm format for sandboxes, query parameters left out of the base string, HMAC-SHA1 instead of HMAC-SHA256, or + used for spaces. Expired or revoked tokens, a role without access token permissions and clock drift cause it too. The Login Audit Trail detail column names the reason.

#### Is NetSuite token-based authentication being retired?

**Answer:** Oracle states that as of 2027.1 no new TBA integrations can be created for SOAP web services, REST web services and RESTlets, while existing integrations continue working. New RESTlet and REST integrations should use OAuth 2.0, either authorization code or the client credentials machine-to-machine flow.

## Search terms

netsuite restlet authentication, netsuite tba signature, netsuite oauth 1.0 hmac-sha256, netsuite restlet post, netsuite rest api postman, netsuite rest api setup, get netsuite api key, netsuite invalid login attempt tba

---
Talk to an engineer: https://netray.co/contact?intent=erp_ai | Machine catalog: https://netray.co/agent/catalog.json
