Published on

NetSuite TBA auth header

Signs a request for NetSuite token-based authentication (TBA). Query string parameters are part of the signature. The realm is the account ID in uppercase with an underscore, for example 1234567_SB1.

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}`
}

Call it once per attempt. A retry that reuses the header fails because the nonce was already used.

TBA cannot be used for new integrations from NetSuite 2027.1. For new work, see the OAuth 2.0 client credentials snippet. Full walkthrough: Setting up NetSuite TBA.