Skip to content

NetSuite OAuth 2.0 setup

How to issue OAuth 2.0 credentials for a Turbobase instance to call NetSuite. The values produced here populate your instance’s NetSuite connection in the console.turbobase.io onboarding flow.

Most sign-ups don’t read this end-to-end. The console walks you through the OAuth 2.0 consent, bundle deploy, and config steps interactively. The click-paths below are the reference for operators who want to drive each NetSuite click by hand — for example, when re-running the flow against a refreshed sandbox.

  • Admin access to the target NetSuite account (sandbox or prod).
  • A modern browser. The whole flow is clicks in the NetSuite UI.

Setup → Company → Enable Features → SuiteCloud tab.

Check the boxes, then Save:

  • OAuth 2.0 (under “Manage Authentication”)
  • REST Web Services (under “SuiteTalk (Web Services)”)
  • Client SuiteScript and Server SuiteScript (under “SuiteScript”)
  • SuiteCloud Development Framework (under “SuiteCloud Development”) — needed so Turbobase can deploy its bundle into your account.

If any are already checked, leave them. If you can’t see this tab, your role is missing the “Enable Features” permission.

Setup → Integration → Manage Integrations → New.

  • Name: Turbobase Sync (any human label).
  • State: Enabled.
  • Authentication tab:
    • Check OAuth 2.0.
    • Under “Authorization Code Grant”, check AUTHORIZATION CODE GRANT and leave PUBLIC CLIENT unchecked. Turbobase uses a confidential client, so copy the client secret NetSuite surfaces alongside the client id.
    • Check CLIENT CREDENTIALS (MACHINE TO MACHINE) GRANT. This is the one box that lets your instance stop depending on the consent’s refresh token: within the hour after you connect, the instance maps its own certificate to this record through NetSuite’s certificate API and mints with it from then on — no reconnect cadence, and no visit to NetSuite’s M2M setup page. Nothing else about the record changes, and the box does not ask you for a certificate; the instance supplies its own. See “Certificate auth” below.
    • Refresh Token Validity (in hours): set this to 720 (NetSuite’s maximum, 30 days). It defaults to 168 — 7 days. Once certificate auth is active this only bounds how long the instance has to complete that switch after a connect; until then it is how often you would have to reconnect. See the note under step 5.
    • Scope: check rest_webservices and restlets. In the UI these are labelled REST Web Services and RESTLETS.
    • Redirect URI: https://console.turbobase.io/ns/auth/callback.
    • Uncheck Token-Based Authentication and the user-credentials option — pure OAuth 2.0 only.
  • Click Save.

NetSuite shows the Client ID (sometimes labeled “Consumer Key” in older UI strings) and the Client Secret once at the top of the next page. Copy them now; the client secret is never shown again. You’ll paste them into the console in step 7.

Already done for you.

The Turbobase bundle adds an integration role to your account when the console deploys it. The role carries the permissions for every record Turbobase syncs.

From bundle 2.18.0 the deploy adds two roles, and you choose which one your user holds. You do not have to do anything to take the first:

Role Shown in NetSuite as What it grants
customrole_tb_io_integration TB Hybrid Integration Everything Turbobase can do, including reading your script execution log and managing script deployments.
customrole_tb_io_integration_min TB Hybrid Integration (min) The same record access, without the two permissions below. It carries one permission the default does not, so that a connection held under it can renew its own authentication.

customrole_tb_io_integration is the default — it is the role this guide assigns, and the one every account that installed before 2.18.0 already has. Nothing changes for you if you leave it alone.

The narrower role, customrole_tb_io_integration_min, drops two permissions: Manage Roles and SuiteScript. Together those two let the role edit any role in your account and deploy server-side code into it, so if your security review will not accept a vendor credential that can do that, this is the role to assign instead. Everything you actually use Turbobase for still works: every record it syncs, in both directions, and every app built on it.

What you give up by choosing it:

  • The script execution log. Turbobase can no longer read NetSuite’s own script execution log, so the diagnostics that quote it are unavailable — the log view in your instance, the “Recent UE webhook errors” health check (which reports a warning instead of a result), and the script detail the Customization Analyzer shows. The AI assistant cannot answer questions about why a script failed on your account.
  • Script deployment changes. Turbobase cannot enable real-time sync on a custom record type on its own; that needs a bundle redeploy from support instead. Real-time sync on standard records is unaffected.

What you keep, and it is the reason this choice is safe to take: the narrower role also carries Manage own OAuth 2.0 Client Credentials Certificates, which the default role does not need because this guide has you consent as Administrator. NetSuite expires the credential from a sign-in on a fixed cadence — 30 days at most — and the way your instance stops depending on it is to map a certificate of its own, under the role that consented (see “Certificate auth” below). The permission is what makes that possible, so a connection consented under the narrower role renews itself exactly like one consented as Administrator. Without it the connection would stop when that credential expired — a week after consent at NetSuite’s default setting, 30 days at the maximum step 2 asks for — and the only way back would be to reconnect as an Administrator, undoing the switch.

To switch an existing connection, assign the other role to your integration user (Setup → Users/Roles → Manage Users, then the user’s Access tab) and run the OAuth consent flow again from the Turbobase console, choosing that role at NetSuite’s role picker in step 5 instead of Administrator — the token stays bound to the role you consented as, so re-consenting is what moves it. Switching back works the same way, and no synced data is affected either way.

For the OAuth consent step below, consent as Administrator — unless you have deliberately chosen the narrower role above, in which case consent as that. The bundle’s deploy needs Administrator-level permissions, and at the current version runtime tokens remain bound to whatever role you consented as. The integration role narrows runtime calls in a later step.

Setup → Users/Roles → Manage Users.

Pick or create a user with the Administrator role assigned. This is the user you’ll authenticate as during the consent step.

For prod, create a dedicated integration user so the runtime OAuth tokens stay bound to a stable account rather than someone’s personal sign-in.

5. Run the OAuth 2.0 authorization-code flow

Section titled “5. Run the OAuth 2.0 authorization-code flow”

The console drives this for you when you connect from the onboarding wizard. To run it manually:

  1. Build the authorize URL:

    https://<account>.app.netsuite.com/app/login/oauth2/authorize.nl
    ?response_type=code
    &client_id=<from step 2>
    &scope=rest_webservices+restlets
    &redirect_uri=<from step 2>
    &state=<random>
    &code_challenge=<base64url(sha256(verifier))>
    &code_challenge_method=S256

    <account> is the account-specific domain (<account-id-lowercased-with-hyphens> — for example 1234567 or 3141592-sb1).

  2. Open the URL in a browser. Sign in as the user from step 4, pick the account from step 4’s user record, and at the role picker, choose Administrator. NetSuite redirects to your redirect_uri with code and state query parameters.

  3. Exchange the code for tokens:

    Terminal window
    curl -X POST https://<account>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    -d 'grant_type=authorization_code' \
    -d 'code=<from redirect>' \
    -d 'redirect_uri=<from step 2>' \
    -d 'code_verifier=<the verifier from step 1>' \
    -d 'client_id=<from step 2>'

    Turbobase’s integration record is a confidential client, so drop client_id from the body and add -u '<client_id>:<client_secret>' instead. NetSuite returns:

    { "access_token": "...", "refresh_token": "...", "expires_in": 3600 }

    Copy the refresh token — it gets pasted into the console in step 7. Access tokens are short-lived (60 min); the runtime mints fresh ones on demand.

Heads-up — the refresh token expires on a fixed cadence, and the instance is designed to stop needing it. Using it returns a fresh 60-minute access token but the same refresh token, so the deadline never moves. When it passes, every /token call answers invalid_grant and sync, sign-in and bundle operations stop together. Re-running this connect flow is the only thing that mints a new one — a retry, a redeploy or a restart cannot. Turbobase reports the exact expiry on the instance’s sync-health page and warns 72 hours ahead.

With CLIENT CREDENTIALS (MACHINE TO MACHINE) GRANT ticked in step 2, this deadline matters only once: within the hour after connecting, the instance uses this consent’s token to map its own certificate to your integration record and switches to the certificate grant, which has no refresh token at all (see “Certificate auth” below). If the box is left unticked, the cadence below is what you live with.

How long you get is a setting on your integration record, not a NetSuite constant. Two fields decide it:

  • Refresh Token Validity (in hours) — how long the token lives. Defaults to 168 (7 days); the maximum is 720 (30 days). Step 2 asks you to set 720.
  • Maximum Time For Token Rotation (in hours) — whether NetSuite issues a replacement refresh token when you use one, which would move the deadline forward.

NetSuite renders the rotation field disabled for a confidential client, so the chain cannot roll and simply lapses at the validity window. Ticking Public Client enables rotation — but measured against a live account, rotation is permitted only until the original consent plus that window, after which the chain stops with invalid_request even though the token’s own expiry is still weeks away. So a public client lands on the same 30-day ceiling while giving up client authentication. 30 days is the ceiling for this flow either way, which is why step 2 keeps the confidential client and just maximises the validity.

Setup → Company → Company Information → Account ID.

For sandboxes the format is like TSTDRV1234567 or 3141592_SB1. Lowercase it for the console field: tstdrv1234567.

The REST hostname derives from the account ID as ${id-with-underscores-replaced-by-hyphens-lowercased}.suitetalk.api.netsuite.com. For 3141592_SB1 that’s 3141592-sb1.suitetalk.api.netsuite.com. The console builds this automatically.

When the OAuth callback lands on the console, it writes the values directly into your instance’s secret store — encrypted at rest, never written to a local file. If you ran the flow manually, paste these into your instance settings on console.turbobase.io:

  • Account ID
  • Client ID
  • Client Secret
  • Refresh Token
  • Access Token (optional but recommended on first run)
  • Access Token expiry (ms-epoch)

If a refresh token ever leaks: revoke it at Setup → Users/Roles → Active Tokens in NetSuite (the same screen lists OAuth 2.0 refresh tokens), then re-run step 5 and update the value in the console.

The client secret is required — Turbobase’s integration record is a confidential client.

Certificate auth — automatic after the first connect

Section titled “Certificate auth — automatic after the first connect”

Everything above produces an authorization-code credential, and NetSuite caps that grant at 30 days from consent however the record is configured. The grant that has no such cap is client credentials: the instance holds a private key, NetSuite holds the matching certificate mapped to your integration record, and every access token is minted from a signed assertion. No refresh token exists to expire.

You do not set this up. Once per hour the instance runs a job that:

  1. checks whether it already holds a certificate NetSuite accepts, and if so does nothing until 30 days before that certificate expires;
  2. otherwise generates a 4096-bit RSA key pair and a two-year self-signed certificate in-process, uploads the certificate through NetSuite’s certificate endpoint using the authorization-code token from your consent, mapped to the same role and entity that consented;
  3. proves the new certificate by minting with it, and only then stores the material in the instance’s encrypted secret store.

Every process picks the certificate up at its next start, and a process whose refresh token dies before then switches over on the spot. Thirty days before the certificate expires the same job uploads a successor using the certificate’s own token, so renewal needs no one either. The previous certificate stays mapped until NetSuite expires it. The only certificate the instance revokes on its own is one it has just uploaded in a run that then failed, so a failed run never leaves an unused certificate on your record.

If NetSuite stops accepting the certificate — it was revoked, the box was unticked, or the employee it acts as was deactivated — the instance falls back to the consent’s refresh token at once, and the next hourly run maps a new certificate from it. If that token has also run out, the run reports BLOCKED and asks for a reconnect; the run after the reconnect maps the new certificate.

If NetSuite answers the hourly token request with a server error instead of refusing it, the instance keeps the certificate and tries again next hour, because a brief NetSuite outage is not a reason to replace a working credential. After four hours of that it stops waiting: it treats the certificate as beyond saving and maps a replacement from the consent’s refresh token, exactly as above — so a connection stuck this way repairs itself within about five hours, with nothing for you to do. If there is no usable refresh token left either, the run reports BLOCKED and asks for a reconnect.

What you have to do: tick CLIENT CREDENTIALS (MACHINE TO MACHINE) GRANT on the integration record (step 2) and consent as an Administrator (step 3). The upload needs the “Manage own OAuth 2.0 Client Credentials certificates” permission, which Administrator carries — and so, from bundle 2.18.0, does customrole_tb_io_integration_min, so a connection consented under the narrower role from § 3 renews itself the same way.

If the box was left unticked, NetSuite refuses the mapping and the job reports BLOCKED in its run output with that exact remedy, then tries again next hour. Tick the box; nothing else needs redoing. The instance keeps working on the authorization-code credential meanwhile, with its 30-day cadence.

Where to see it: the instance’s sync-health page reports the credential in force — a refresh token with its expiry, or a certificate with its notAfter — under the same NetSuite credential check, so the countdown gauge and alerts keep working without knowing which grant is live. When the hourly job cannot get a working certificate for your account — NetSuite refused the mapping, or the credential it maps from has itself been refused — a second red check, NetSuite certificate auth, appears beside it: it names the integration-record setting to change, carries the hourly job’s own words about which of the two NetSuite said, and disappears by itself on the first run that succeeds — there is nothing to dismiss. That is the row to look for whenever the credential countdown is going down instead of being renewed. In NetSuite, Setup → Integration → Manage Authentication → OAuth 2.0 Client Credentials (M2M) Setup lists the mapping. Revoking a certificate there does not by itself cut the instance off: while the consent’s refresh token is still valid, the instance falls back to it and maps a new certificate within the hour. To cut the instance off deliberately, untick CLIENT CREDENTIALS (MACHINE TO MACHINE) GRANT and revoke the certificate, or disable the integration record.

Saving the credentials tests the connection and then starts the first sync in the background — the console doesn’t wait for it, so the wizard moves on as soon as the connection answers. Watch the first pull arrive under Activity (/data/sync/history) in your app, and read anything that went wrong on Health (/data/sync/health) or /data/sync/failures — see sync health. The three errors you’re most likely to meet here:

  • 401 INVALID_LOGIN_ATTEMPT on a RESTlet call means the OAuth bearer was minted under a role the script deployment doesn’t audience — almost always because the user picked a role other than the Turbobase integration role at OAuth consent time. Re-run step 5.
  • invalid_grant from /token almost always means the refresh token has passed its validity window — 7 days from consent at NetSuite’s default, 30 if you set the maximum. See the heads-up under step 5 for the two fields that set it. Check the sync-health page, which reports the exact expiry instant. Re-run the connect flow from the console to mint a new one; nothing else recovers it.
  • 403 Permission Violation means the integration role is missing a record-level permission. Contact support@turbobase.io — a role refresh from the console fixes it.

9. Pin the Turbobase portlet on your NetSuite dashboard

Section titled “9. Pin the Turbobase portlet on your NetSuite dashboard”

The Turbobase bundle ships a portlet that lists the Turbobase apps each person is allowed to open, and opens any of them already signed in. NetSuite does not auto-pin portlets — every user who wants it has to add it to their dashboard once:

  1. Open NetSuite home (the dashboard you land on after signing in).
  2. Click Personalize in the top-right.
  3. Click Add Content in the sidebar that opens.
  4. Find Turbobase in the list (search for “turbobase” if needed) and click it.
  5. Drag the new portlet to the column you want it in. Click Done.

If Turbobase isn’t in the Add Content list, the bundle install was likely partial — request a redeploy from the console or email support@turbobase.io.

Once the portlet is pinned it shows the apps shared with whoever is looking at the dashboard — the same cards they see on the Apps page in Turbobase, with each app’s own icon and its full description, each opening that app in a new tab, already signed in. Everyone sees their own list and nobody sees an app they were not given. The Turbobase icon above the list opens Turbobase itself in a new tab, already signed in, and the Build the App button beside it opens the Apps page, where you can describe a new app at the top. The local sign-in at /login is the always-available fallback. Three things worth knowing:

  • It needs bundle version 2.17.3 or later. On an older bundle the portlet is called Turbobase Access and may show only an Open Turbobase button, show Your apps could not be loaded with HTTP status 406, show each app as two initials with no Build the App button, or show the cards with the label chips that were on them when your instance last upgraded, so a label added since then shows on the Apps page and not here. Request a redeploy from the console to pick it up.
  • A newly shared app can take up to an hour to appear there, because each person’s list is remembered for an hour so the dashboard is not slowed down. It opens from Turbobase itself straight away.
  • If the list cannot be loaded — your instance is briefly away, or signing in did not answer — the portlet shows an Open Turbobase button instead, which takes you to Turbobase already signed in.

Whichever NetSuite environment you connect (sandbox or production), the 14-day learning window builds your instance’s rule book from real write rejections.

  • Have a sandbox? Start in the sandbox-first path — refresh the sandbox from production, learn safely for 14 days, then carry the rule book into a fresh production-pointing instance.
  • No sandbox or smaller account? Use the production-direct path. The same reactive loop runs against real traffic; watch the failures page in your app more attentively during the first week.

Questions? Email support@turbobase.io.