# NetSuite OAuth 2.0 Client Credentials (M2M) JWT Code Generator

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

## Short answer

Generate the openssl commands, the signed JWT client assertion and the token request for NetSuite machine-to-machine OAuth 2.0, in Python and Node.js.

## 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:** Sandbox 1234567_SB1 or 1234567-sb1; the code converts it to the hostname form.

#### Client ID (integration record)

**Key:** client_id

**Type:** text

**Default:** <client_id>

**Help:** Shown once when you save the integration record with Client Credentials (Machine to Machine) Grant checked.

#### Certificate ID (kid)

**Key:** certificate_id

**Type:** text

**Default:** <certificate_id>

**Help:** From the mapping on the OAuth 2.0 Client Credentials (M2M) Setup page.

#### Signing algorithm

**Key:** algorithm

**Type:** select

**Options:**

- PS256
- PS384
- PS512
- ES256
- ES384
- ES512

**Default:** PS256

**Help:** Use PS* with an RSA key and ES* with an EC key. RS256 is not in the allowed list.

#### Scope

**Key:** scope

**Type:** select

**Options:**

- rest_webservices
- restlets
- rest_webservices,restlets
- suite_analytics

**Default:** rest_webservices

**Help:** Must be enabled on the integration record.

#### Private key file

**Key:** private_key_path

**Type:** text

**Default:** ./netsuite_private.pem

**Blocks:**

#### 1. Create the key pair and certificate (openssl)

**Body:** # RSA key for PS256 / PS384 / PS512
openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 365 \
  -keyout {{private_key_path}} -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 {{private_key_path}} -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

**Body:** # pip install pyjwt cryptography requests
import time
import uuid

import jwt
import requests

ACCOUNT_HOST = "{{account_id}}".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 = "{{algorithm}}"
SCOPE = "{{scope}}"

_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("{{private_key_path}}", "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

**Body:** // 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 = '{{account_id}}'.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: '{{scope}}', aud: tokenUrl, iat: now, exp: now + 3000, jti: crypto.randomUUID() },
    fs.readFileSync('{{private_key_path}}'),
    { algorithm: '{{algorithm}}', 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) + '...'));

**Notes:** 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.

## Content

**Intro:** 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.

**Sections:**

#### How to use it

**Body:** 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.

**Bullets:**

- 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

**Body:** 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

**Body:** 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

**Body:** 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.

**Cta:** 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.

**Faq:**

#### How do I set up the NetSuite REST API with OAuth 2.0 client credentials?

**Answer:** 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?

**Answer:** 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?

**Answer:** 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?

**Answer:** 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.

## Search terms

netsuite oauth 2.0 client credentials, netsuite m2m jwt, netsuite rest api setup, netsuite restlet authentication, rest api netsuite, netsuite rest web services, netsuite client_assertion jwt, netsuite oauth2 certificate id

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