NetSuite OAuth 2.0 Client Credentials (M2M) JWT Code Generator
This generator writes the full NetSuite OAuth 2.0 client credentials (machine-to-machine) flow: openssl commands for the certificate, a signed JWT client assertion, the POST to the token endpoint and a Bearer call, in Python and Node.js. It is for integration developers moving off TBA or setting up a new server-to-server connection.
Fill in
Sandbox 1234567_SB1 or 1234567-sb1; the code converts it to the hostname form.
Shown once when you save the integration record with Client Credentials (Machine to Machine) Grant checked.
From the mapping on the OAuth 2.0 Client Credentials (M2M) Setup page.
Use PS* with an RSA key and ES* with an EC key. RS256 is not in the allowed list.
Must be enabled on the integration record.
1. Create the key pair and certificate (openssl)
# RSA key for PS256 / PS384 / PS512 openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 365 \ -keyout ./netsuite_private.pem -out netsuite_public_cert.pem -subj "/CN=netsuite-m2m" # or an EC P-256 key for ES256 openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -sha256 -nodes -days 365 \ -keyout ./netsuite_private.pem -out netsuite_public_cert.pem -subj "/CN=netsuite-m2m" # Upload netsuite_public_cert.pem at Setup > Integration > Manage Authentication > # OAuth 2.0 Client Credentials (M2M) Setup, then copy the Certificate ID shown for the mapping.
2. Python: sign the JWT, get and cache the access token
# pip install pyjwt cryptography requests
import time
import uuid
import jwt
import requests
ACCOUNT_HOST = "1234567".lower().replace("_", "-")
TOKEN_URL = "https://" + ACCOUNT_HOST + ".suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token"
CLIENT_ID = "<client_id>"
CERTIFICATE_ID = "<certificate_id>"
ALGORITHM = "PS256"
SCOPE = "rest_webservices"
_cache = {"token": None, "expires_at": 0}
def get_access_token():
if _cache["token"] and time.time() < _cache["expires_at"] - 300:
return _cache["token"]
with open("./netsuite_private.pem", "rb") as f:
private_key = f.read()
now = int(time.time())
claims = {
"iss": CLIENT_ID,
"scope": SCOPE,
"aud": TOKEN_URL,
"iat": now,
"exp": now + 3000, # must be less than 60 minutes after iat
"jti": str(uuid.uuid4()),
}
assertion = jwt.encode(claims, private_key, algorithm=ALGORITHM,
headers={"kid": CERTIFICATE_ID, "typ": "JWT"})
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,
}, timeout=60)
if resp.status_code != 200:
raise RuntimeError(str(resp.status_code) + " " + resp.text)
body = resp.json()
_cache["token"] = body["access_token"]
_cache["expires_at"] = time.time() + int(body["expires_in"])
return _cache["token"]
if __name__ == "__main__":
token = get_access_token()
# Example call with rest_webservices scope
r = requests.get("https://" + ACCOUNT_HOST + ".suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1",
headers={"Authorization": "Bearer " + token}, timeout=60)
print(r.status_code, r.text[:500])3. Node.js: same flow with jsonwebtoken
// npm install jsonwebtoken (Node.js 18 or later for built-in fetch)
const fs = require('fs');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const accountHost = '1234567'.toLowerCase().replace(/_/g, '-');
const tokenUrl = 'https://' + accountHost + '.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token';
async function getAccessToken() {
const now = Math.floor(Date.now() / 1000);
const assertion = jwt.sign(
{ iss: '<client_id>', scope: 'rest_webservices', aud: tokenUrl, iat: now, exp: now + 3000, jti: crypto.randomUUID() },
fs.readFileSync('./netsuite_private.pem'),
{ algorithm: 'PS256', header: { kid: '<certificate_id>', typ: 'JWT' } }
);
const res = await fetch(tokenUrl, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: assertion,
}),
});
if (!res.ok) throw new Error(res.status + ' ' + (await res.text()));
const body = await res.json();
return { token: body.access_token, expiresIn: body.expires_in };
}
getAccessToken().then((t) => console.log('expires_in', t.expiresIn, 'token', t.token.slice(0, 20) + '...'));Verify for your release: allowed alg values (Oracle lists PS256, PS384, PS512, ES256, ES384, ES512), the scope list, and certificate rules such as key size and maximum validity on the setup page. exp must be less than 60 minutes after iat. The access token is valid for 60 minutes (expires_in 3600) and there is no refresh token in this flow: sign a new assertion when it expires. Keep the private key out of source control.
Moving off TBA before 2027.1?
Send us your list of integrations and we will map which ones need an OAuth 2.0 client credentials setup and in what order.
Optional. The tool and every export work without this. No spam.
How to use it
First create an integration record (Setup > Integration > Manage Integrations > New) with Client Credentials (Machine to Machine) Grant checked and the scopes you need, and copy the client ID. Run block 1 to create a private key and a self-signed certificate. Then open Setup > Integration > Manage Authentication > OAuth 2.0 Client Credentials (M2M) Setup, create a mapping of entity, role and application, upload the public certificate, and copy the Certificate ID. Enter the client ID, certificate ID, algorithm and scope here and run block 2 or 3. A 200 response with access_token means the setup is complete.
- PS256 needs the RSA command, ES256 the EC command.
- Certificates have an end date: upload a replacement and switch the kid before the old one expires.
- After a sandbox refresh, confirm the mapping and certificate exist in the sandbox before testing.
What the JWT must contain
Oracle documents the request token structure precisely. Header: typ is JWT, alg is one of PS256, PS384, PS512, ES256, ES384 or ES512, and kid is the Certificate ID from the mapping. Payload: iss is the client ID, scope is restlets, rest_webservices, suite_analytics or a comma-separated combination, aud is the token endpoint https://<account>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token, iat is the issue time in epoch seconds and exp must be less than 60 minutes after iat. The code uses 50 minutes and adds a random jti. The JWT is sent form-encoded as client_assertion with grant_type=client_credentials and the fixed jwt-bearer client_assertion_type.
Reading the token response
A good response returns access_token (itself a JWT), token_type bearer and expires_in 3600, meaning the token lasts 60 minutes. There is no refresh token in this grant, so when it expires you sign a new assertion and ask again; the Python block caches the token and renews it five minutes early. Send it as Authorization: Bearer <token> to REST web services or RESTlets, depending on the scope you requested. Requests after expiry fail, and the token endpoint returns invalid_grant for a bad or expired assertion, so log the response body when you get anything other than 200.
Common mistakes
Using RS256 is the frequent one: RSA PKCS1 v1.5 is not in Oracle's allowed algorithm list, so sign with PS256 over the same RSA key. Others: kid set to the client ID instead of the Certificate ID, aud pointing at the REST base URL instead of the token endpoint, exp set to an hour or more, a scope on the JWT that is not enabled on the integration record, the mapping created for a role without the needed permissions, an expired or revoked certificate, and a sandbox account ID written with an underscore in the hostname. Check Setup > Integration > Manage Authentication and the Login Audit Trail for the reason.
Frequently Asked Questions
How do I set up the NetSuite REST API with OAuth 2.0 client credentials?
Enable OAuth 2.0 and REST web services in Enable Features, create an integration record with the client credentials grant and scopes, generate a certificate, map it to an entity and role on the OAuth 2.0 Client Credentials (M2M) Setup page, then sign a JWT with the private key and exchange it at the token endpoint for a 60-minute access token.
Which signing algorithms does NetSuite accept for the client credentials JWT?
Oracle lists PS256, PS384 and PS512 (RSA-PSS) and ES256, ES384 and ES512 (ECDSA). RS256, which uses RSA PKCS1 v1.5, is not on that list, so an RSA key must be used with a PS algorithm. Match the alg to the key type you generated.
Does the client credentials flow return a refresh token?
No. The response holds access_token, token_type and expires_in, which is 3600 seconds. When the token expires you create a new signed assertion and request a new token. That is cheap, so cache the token for its lifetime rather than requesting one per call.
Can I use client credentials for RESTlets and SuiteAnalytics Connect?
Yes, if the integration record has the matching scopes: restlets for RESTlet calls, rest_webservices for the REST record and query services, and suite_analytics for Connect. Request them in the scope claim, comma-separated. The mapped role still needs the permissions each call or query requires.
If you want a private assistant or agent that calls NetSuite with its own M2M role and keeps keys on your servers, we can build and run it with you.
Related Tools
NetSuite Token-Based Auth OAuth 1.0 Signature Code Generator
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.
Developer ToolsNetSuite OAuth 2.0 Access Token Refresh Calculator
Work out how often your integrations refresh NetSuite OAuth 2.0 access tokens and how many API calls each token serves.
Developer ToolsNetSuite REST API SuiteQL Request Generator
Turn a SuiteQL statement into a ready POST to the REST query/v1/suiteql endpoint, plus curl and a Python script that pages through every result.
Developer ToolsNetSuite REST API Sales Order and Journal Entry Request Generator
Fill in IDs and amounts to get correct record/v1 JSON for a sales order, a two-line journal entry, a sales order to invoice transform and a C# call.
NetSuiteNetSuite SuiteAnalytics Connect ODBC and JDBC Connection String Generator
Enter your service host, account ID and role ID to get a working JDBC URL, Windows DSN settings, ODBC connection strings, a Linux odbc64.ini stanza and test queries.
More Developer Tools tools
See all- NetSuite SuiteScript 2.1 RESTlet Skeleton Generator
- NetSuite SuiteScript Governance Units by Script Type
- NetSuite API Retry Backoff Wait Time Calculator
- NetSuite REST Web Services Headers and Query Parameters Reference
- SuiteScript 1.0 to 2.1 API Mapping Reference for NetSuite
- SuiteScript 2.1 Scheduled Script Generator for NetSuite
- SuiteScript 2.1 Client Script Generator for NetSuite
- SuiteScript 2.1 Suitelet Form Generator for NetSuite
- NetSuite SuiteScript N/query runSuiteQLPaged Snippet Generator
Keep exploring
Free tools
- NetSuite OAuth 2.0 Access Token Refresh CalculatorWork out how often your integrations refresh NetSuite OAuth 2.0 access tokens and how many API calls each token serves.
- NetSuite Token-Based Auth OAuth 1.0 Signature Code GeneratorGet 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.
- SuiteScript 2.1 Client Script Generator for NetSuiteGenerate a working client script for a transaction or customer form: field checks, line validation with a dialog, sublist totals and a save guard, exporting only the entry points you pick.