---
name: netsuite-oauth-app
description: Creates a NetSuite integration record and obtains OAuth 2.0 client ID and client secret (the consumer key and consumer secret) for a platform that connects many customers' NetSuite accounts — covering the account that owns the integration record and how it reaches other accounts, the account-specific hostnames, the features and role permissions that must exist before a token works, the three coarse scopes, the seven-day refresh token, and a safe credential handoff. Use when asked to get NetSuite or Oracle NetSuite OAuth credentials, create a NetSuite integration record, set up SuiteTalk REST authentication, rotate a NetSuite client secret, or fix a NetSuite OAuth error like `InvalidRedirectURI`, `UnknownIntegration`, `IntegrationBlocked`, `ScopeMismatched`, `invalid_grant`, `invalid_token` / "Invalid login attempt", or a 403 permission violation on a token that just authorized fine. For any other vendor's developer portal, use that vendor's skill instead.
---

# NetSuite OAuth 2.0 App Registration

Get a working NetSuite OAuth 2.0 client — an integration record, redirect URIs, scopes, and the client ID and client
secret — for a platform that connects other organizations' NetSuite accounts on behalf of many customers.

NetSuite is not like the other vendors in this collection, and four things account for almost all of the lost time.

**There is no central developer portal.** The "app" is an *integration record*, and an integration record lives
**inside a NetSuite account** — yours. You create it once, in an account you control, and NetSuite then installs a copy
of it into each customer account the first time someone there authorizes. You do **not** ask every customer to create
their own. That is §1 and §3, and it is the single answer that decides the whole onboarding shape, so read §1 before
you promise anyone anything.

**Every host is account-specific.** The authorize host, the token host and the API host all contain the customer's
account ID, and sandbox account IDs mangle into the hostname by a documented rule. Nothing about a NetSuite host can be
hardcoded. That is §7.

**A valid token is not access.** NetSuite will hand you a perfectly good access token and then return 401s or permission
errors, because a *feature* is off in the customer's account or because the *role* the user picked on the consent screen
does not carry the REST Web Services permission. Neither is visible at registration time. That is §8.

**The refresh token lasts seven days and Oracle does not document it rotating.** For a confidential client the token
endpoint's refresh response is documented as returning an access token only — no new refresh token. Plan the reconnect
story before you build, not after the first weekend. That is §9.

## Inputs to collect before you start

Ask in one batch. Never invent.

| Input | Notes |
| --- | --- |
| **Which NetSuite account will own the integration record** | Production, a partner/developer account, or a sandbox — this decides who can ever reset the secret (§2) |
| **Administrator credentials for that account** | Creating an integration record and enabling features are Administrator tasks (§2, §8) |
| **Integration record name** and description | Customers see this on the consent screen; it becomes read-only in installed copies (§3) |
| **Redirect URIs** | Every callback host your platform serves — HTTPS only (§4) |
| **Scope set** | `rest_webservices`, `restlets`, `suite_analytics` — pick the narrowest that works (§5) |
| **Confidential or public client?** | Confidential gives a 7-day refresh token; public gives a configurable, rotating, one-time-use one and no client secret (§9) |
| **A test customer account you can actually authorize against** | Ideally one you do not own, to prove the cross-account install (§11) |
| **Does the client re-authorize before the refresh token dies?** | Blocking — a connector that only refreshes will lose every connection in a week (§9) |
| **New integration record or an edit to an existing one?** | A new client ID orphans every existing customer connection (§1) |

## Quick Start

1. Establish that you are creating **one** integration record, in your own account, not one per customer (§1).
2. Sign in as Administrator to the NetSuite account that will own it (§2).
3. Enable the OAuth 2.0 feature in that account, and confirm the customer-side feature list (§8).
4. Create the integration record: Authorization Code Grant, scopes, redirect URIs (§3).
5. Add **every** redirect URI exactly (§4).
6. Check the scope boxes that match the scope strings your authorize URL will send (§5).
7. Save once and capture the client ID and client secret — they are shown **only once** (§6).
8. Work out how the customer's account ID reaches every URL before writing client code (§7).
9. Confirm the role and permission story with a real customer role, not Administrator (§8).
10. Decide the reconnect strategy for a seven-day refresh token (§9).
11. Verify end-to-end against a second account (§11), then hand the credentials over (§12).

## Platform state (verified 2026-09-20 — re-verify before trusting)

NetSuite's OAuth 2.0 surface has been stable in shape for years, but three things on this page are recent or dated and
will move.

- **There is no netsuite.com developer console for OAuth apps.** Registration happens at
  *Setup > Integration > Manage Integrations > New* inside a NetSuite account. Oracle's docs live on
  `docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/`, are deep-linked by opaque section IDs, and those IDs
  occasionally move — every link in **References** was checked on 2026-09-20.
- **PKCE becomes mandatory for all new integrations as of 2027.1.** Today `code_challenge` is optional for confidential
  clients, and required for public clients and for the `mcp` scope. Build the PKCE path now; it will stop being
  optional (§5).
- **There is a fourth scope, `mcp`, and it cannot be combined with any other.** Oracle lists the allowed scope values as
  `restlets`, `rest_webservices`, `suite_analytics` and `mcp`, and documents that `mcp` must be used alone and requires
  `code_challenge` for both public and confidential clients. `prompt=none` cannot be used with it.
- **`plain` PKCE has not been supported since 2020.2.** `code_challenge_method` must be `S256`.
- **Token lifetimes**: access token 60 minutes; refresh token **seven days** for confidential clients; **two days,
  one-time use, configurable 1–720 hours** for public clients. The id_token is only relevant to the separate
  "NetSuite as OIDC Provider" flow.
- **OAuth 2.0 authorizations do not cross environments.** An authorization made in a production account is not copied to
  Release Preview or to a sandbox, and each sandbox refresh clears what was there. Every environment is its own
  authorization (§7).

If the UI does not look like this — in particular if *Manage Integrations* is missing, which usually means the OAuth 2.0
feature is off — stop and report what you actually see rather than clicking on.

## 1. One integration record, or one per customer? — the crux

**One.** You create a single integration record in a NetSuite account you control, and its OAuth 2.0 client ID is the
identifier every customer authorizes against. You do not ask customers to create integration records, and you do not
collect a consumer key and secret from each of them.

What makes that work is Oracle's **auto-installation** behaviour. The integration-record documentation says a record can
be distributed to other accounts by bundling or by auto-installation, and the auto-installation page states plainly:

> "Auto-installation works with integration records that use user credentials, token-based auth, or OAuth 2.0 for
> authentication."

> "Whether using The Three-Step TBA Authorization Flow, calling The IssueToken Endpoint, or using OAuth 2.0, an
> Integration record is automatically installed in your account."

So the first time a user in a customer account completes the authorization code flow against your client ID, a copy of
your integration record appears in *their* Manage Integrations list, and their account is connected. Nothing is typed in
by the customer.

Five consequences that decide real onboarding decisions:

- **The owning account owns the secret.** Only an authorized user in the account where the record was created can reset
  the client ID and secret. Records installed elsewhere are considered owned by a different account and are not fully
  editable — the installing account can change only **Note** and **State**; the owner's changes sync outward. Pick the
  owning account deliberately in §2, because you cannot move it later.
- **Name and description become read-only in the customer's copy**, and **State, Note and OAuth 2.0 Consent Policy are
  per-account values** that may differ from yours once installed.
- **A customer can block you before you ever see a request.** The *Require Approval during Auto-Installation of
  Integration* preference (*Setup > Integration > Integration Management > SOAP Web Services Preferences*) defaults to
  **False**, which enables newly installed records immediately. Set to **True**, your record installs as **Waiting for
  Approval** and requests are blocked until an administrator enables it. The symptom is `IntegrationBlocked` (§11).
- **Sandboxes and Release Preview are separate accounts for this purpose.** Authorizations are not copied from
  production, and a sandbox refresh clears them. A customer testing in a sandbox authorizes there separately.
- **The client credentials (machine-to-machine) flow does not get this.** Oracle states the client credentials setup in
  one account "isn't copied to any other production account, Release Preview account, or sandbox account" and must be
  set up explicitly in each. That flow is per-customer work — certificate upload plus an entity/role/application
  mapping — which is exactly why the authorization code grant is the right shape for a multi-customer connector.

**Verify this on a real customer account before you build onboarding around it.** Oracle's auto-installation page is
written mostly around SOAP and token-based authentication and names OAuth 2.0 in one sentence; the behaviour is
documented but it is worth one round trip against an account you do not own (§11).

### Reuse or register new?

A new integration record means a **new client ID, and every existing customer connection is bound to the old one** —
every customer would have to reconnect, and your record would auto-install a second time into their accounts. Reuse the
existing record for: adding a redirect URI, adding a scope, rotating a compromised secret, or diagnosing a failure.
Register a new one only when the user explicitly wants one — a replacement for a compromised record, a separate product,
or a deliberate test record in a sandbox. Say which path you are taking first.

## 2. Account: which NetSuite account owns the record

There is no free self-serve signup for a NetSuite developer account of the kind other vendors offer. In practice the
owning account is one of:

- **Your company's own NetSuite production account** — the usual answer if you run NetSuite. The secret can be reset by
  your administrators, forever.
- **A partner or developer account** obtained through Oracle's NetSuite partner programs. Note that Oracle documents
  development and partner accounts as having a fixed concurrency base limit of **5** that does *not* scale with
  SuiteCloud Plus licenses (§10) — fine for building, not for load testing.
- **A sandbox** — acceptable for a throwaway test record, wrong for the record customers will authorize against, because
  sandbox refreshes are destructive.

You need the **Administrator** role (or a role with Core Administration Permissions) in that account to enable the
OAuth 2.0 feature and to create the integration record.

Hand control back to the user for anything only they can do: obtaining the account, two-factor enrolment, accepting the
SuiteCloud Terms of Service, or deciding which account will own the record permanently. Do not retry a blocked step in a
loop.

**If this session has no browser automation** (the usual case for a CLI or cloud run), do not pretend to click. Hand the
user an exact, ordered click path with the literal values to paste (§4 redirect URIs, §5 scope boxes), warn them in
advance that the client ID and secret appear **only once** on save (§6), and continue once they report back.

## 3. Create the integration record

*Setup > Integration > Manage Integrations > New*.

| Field | What to put, and why it matters |
| --- | --- |
| **Name** | The product name customers will see on the consent screen. Becomes **read-only** in every installed copy |
| **Description** | Optional; also read-only once installed |
| **State** | **Enabled** |
| **Authorization Code Grant** | **Check it.** This is the browser flow a multi-customer connector needs |
| **Client Credentials (Machine to Machine) Grant** | Leave unchecked unless you specifically need M2M — it is per-customer setup work (§1) |
| **Redirect URI** | One or more; see §4 |
| **Scope: REST Web Services / RESTlets / SuiteAnalytics Connect / NetSuite AI Connector Service** | Check exactly the ones your authorize URL will send (§5) |
| **Public Client** | Leave **unchecked** for a server-side connector — see the trade-off below and in §9 |
| **OAuth 2.0 Consent Policy** | `Always Ask` (default), `Never Ask`, or `Ask First Time`. `Never Ask` is auto-approval by an Administrator and is a per-account value that may change once installed |
| **Application Logo / Terms of Use / Privacy Policy** | Optional. Logo is JPEG/PNG/GIF; the two policy documents are PDFs taken from the File Cabinet, so upload them there first |
| **Dynamic Client Registration** | Only for clients that register without knowing the client ID in advance; requires **Public Client**. Not what a connector needs |

Things that cost a cycle if you get them wrong:

- **Authorization Code Grant and Client Credentials can both be checked**, but checking Client Credentials does not save
  you any per-customer work — that flow still needs certificate and mapping setup in every account (§1).
- **Public Client is not a small choice.** It removes the client secret, makes PKCE mandatory, and switches the refresh
  token to a **two-day, one-time-use** token that you can configure between 1 and 720 hours, with a separate *Maximum
  Time For Token Rotation* setting (default 168 hours). For a confidential client the refresh token is fixed at seven
  days and Oracle does not document a way to extend it. That makes "public client with a 30-day rotating refresh token"
  a genuinely tempting option for a long-lived connector — but it means no client secret and a strict one-time-use
  rotation discipline. Raise it as a decision; do not silently pick one (§9).
- **If you check NetSuite AI Connector Service**, Oracle says to clear all other OAuth 2.0 scope boxes and Token-based
  Authentication. It is not additive.
- **Save once.** The credentials appear on that save and only that save (§6).

## 4. Redirect URIs

Add **every** callback host your platform serves. The value sent as `redirect_uri` must match one of them **exactly**.
For Unified.to these are one per data center; confirm the current list with the platform owner rather than assuming:

```
https://api.unified.to/oauth/code          # us (default)
https://api-eu.unified.to/oauth/code       # eu
https://api-au.unified.to/oauth/code       # au
https://api-dev.unified.to/oauth/code      # dev
```

Rules:

- **HTTPS only.** Oracle states `http://` is not supported; a custom URL scheme such as `myapp://callback` is accepted
  for native clients. Transport security must be guaranteed.
- **The field takes more than one URI**, and the values are validated when you save the record. Oracle does not publish
  a maximum count — if a paste is rejected, that is the thing to report, not to guess around.
- **A mismatch does not redirect.** This is the NetSuite-specific trap: if `redirect_uri` does not match the integration
  record, **no redirect happens at all**. The user sees a NetSuite error page, your callback is never called, and your
  platform sees nothing — not an `error=` parameter, not a failed connection, nothing. The audit-trail name for it is
  `InvalidRedirectURI`. Any "the user clicks connect and just lands on a NetSuite error screen" report starts here.
- **The `redirect_uri` sent at the token exchange must match the one used to get the code**, or the exchange returns
  `invalid_grant`.

## 5. Scopes

NetSuite's scopes are few and coarse. There is no per-record or per-object scope; one scope buys the whole REST surface
that the user's role permits.

| Scope string | Grants | Integration record checkbox |
| --- | --- | --- |
| `rest_webservices` | SuiteTalk REST — the record service **and** SuiteQL over REST | **REST Web Services** |
| `restlets` | RESTlets (custom SuiteScript endpoints) | **RESTlets** |
| `suite_analytics` | SuiteAnalytics Connect (ODBC/JDBC/NetSuite2.com data source) | **SuiteAnalytics Connect** |
| `mcp` | The NetSuite AI Connector service. **Must be sent alone**, requires `code_challenge`, and cannot be used with `prompt=none` | **NetSuite AI Connector Service** |

Mechanics of the authorize request (§11 has the failure table):

- Scopes are **space-delimited** in the `scope` query parameter, and URL-encoded like everything else.
- The scope you send must be **enabled on the integration record**. A mismatch between the token's scope and the
  record's checked boxes surfaces as `ScopeMismatched`, both at authorization and later in the Login Audit Trail.
- `response_type` must always be `code`; `client_id` and `redirect_uri` are required.
- **`state` is required and must be 22–1024 characters**, printable ASCII, unique per flow. This is unusual — most
  vendors treat `state` as optional and unconstrained. A short state fails with `InvalidState`, and a state longer than
  1024 characters is out of spec too, which matters if you pack a signed token into it.
- `code_challenge` / `code_challenge_method` are optional today for confidential clients, **required for public clients
  and for `mcp`, and required for all new integrations as of 2027.1**. `code_challenge_method` must be `S256`. The
  `code_verifier` is 43–128 characters from `A-Z a-z 0-9 - . _ ~`.
- `prompt` accepts `none`, `login`, `consent`, `login consent` or `consent login`. `login` only works against the
  account-specific domain, not the generic one (§7).

### Which scope for which read

- **REST record service** (`/services/rest/record/v1/...`) — `rest_webservices`.
- **SuiteQL over REST** (`POST /services/rest/query/v1/suiteql`, with the required `Prefer: transient` header) — also
  `rest_webservices`. SuiteQL through REST is part of REST web services; it does **not** need `suite_analytics`. It does
  enforce the same role-based access restrictions as SuiteAnalytics Workbook, and returns at most 100,000 results across
  all pages unless the SuiteAnalytics Connect feature is enabled in the account.
- **SuiteAnalytics Connect** (ODBC/JDBC against NetSuite2.com) — `suite_analytics`, and a role permission of its own
  (§8). A REST-only connector does not need this scope and should not ask for it.

**Product fact.** As of 2026-09-20, the connector requests exactly one scope for NetSuite — **`rest_webservices`** — for
every unified object it supports, read and write alike, with a space delimiter. It never requests `restlets`,
`suite_analytics` or `mcp`. So the integration record needs the **REST Web Services** box checked and nothing else, and
a customer whose data the connector reaches through SuiteQL is still covered by that one scope. Confirm with the
connector's owner before checking extra boxes "to be safe" — an unnecessary scope on the consent screen is a support
conversation you do not need.

## 6. Capture the credentials

On **Save**, NetSuite shows the client ID and client secret **once**.

- **Shown only once.** Oracle is explicit: the values cannot be retrieved later, and if lost you must reset them on the
  integration record to get new ones. Warn the human *before* they click Save, and have somewhere to paste ready.
- **Reset is not free.** Resetting produces a new client ID and secret and breaks every existing customer connection.
  Only an authorized user in the **owning** account can reset them (§1).
- **The vocabulary is doubled.** NetSuite labels these *Consumer Key / Client ID* and *Consumer Secret / Client Secret*.
  A request for "the NetSuite consumer key and consumer secret" is a request for the OAuth 2.0 client ID and secret.
  Do not confuse them with the *Application ID* — a separate 32-character value on the same record, used by the older
  user-credentials SOAP path.
- **Endpoints** (all account-specific — see §7):
  - authorize `https://<accountID>.app.netsuite.com/app/login/oauth2/authorize.nl`
  - generic authorize, when the account ID is not yet known:
    `https://system.netsuite.com/app/login/oauth2/authorize.nl`
  - token `https://<accountID>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`
  - revoke `https://<accountID>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/revoke`
  - REST base `https://<accountID>.suitetalk.api.netsuite.com/services/rest`
  - RESTlets `https://<accountID>.restlets.api.netsuite.com`
- **Client authentication** at both the token exchange and the refresh is HTTP Basic over
  `application/x-www-form-urlencoded`: `Authorization: Basic <Base64url(client_id:client_secret)>`. Public clients
  either omit the header and put `client_id` in the body, or send `Basic <Base64url(client_id:)>` — the trailing colon
  still matters.
- **Revocation** takes the **refresh token** in a `token` form field, and Oracle documents that revoking a refresh token
  also invalidates the access tokens issued from it.

Report the secret once so the user can paste it into their secret store, say plainly that it is now in the transcript
and can be reset, then move on.

**Product fact.** As of 2026-09-20, the connector exchanges the code with a form POST carrying `redirect_uri`, `code`
and `grant_type`, authenticating with HTTP Basic — it does **not** put the client ID or secret in the body — and
refreshes with a form POST carrying `refresh_token` and `grant_type`, again over Basic. It **does** send PKCE
(`code_challenge_method=S256`). It has the NetSuite revoke endpoint available but the authorize request it builds sends
only `client_id`, `redirect_uri`, `response_type`, `state` and `scope` plus the PKCE pair. Confirm with the connector's
owner before assuming any of it.

## 7. Account-specific hosts — nothing can be hardcoded

Every NetSuite URL that matters contains the customer's account ID, so the connector must build hosts per connection.

```
https://<accountID>.app.netsuite.com            # authorize / UI
https://<accountID>.suitetalk.api.netsuite.com  # token, revoke, REST
https://<accountID>.restlets.api.netsuite.com   # RESTlets
```

The account ID is not always a plain number, and the mapping into the hostname is a documented transformation. Oracle's
rule, verbatim: **"the underscore becomes a hyphen and the capital letters of the acronym become lowercase letters."**

| Account ID (as the customer sees it) | Hostname prefix |
| --- | --- |
| `1234567` (production) | `1234567` |
| `1234567_SB1` (sandbox) | `1234567-sb1` |
| `1234567_SB` (sandbox) | `1234567-sb` |
| `1234567_RP` (Release Preview) | `1234567-rp` |

Where the account ID comes from, and why you may not need to ask:

- A customer can read it at *Setup > Company > Company Information*, and it is the first label in every NetSuite URL
  they use.
- **You can start the flow without it.** Oracle documents a generic authorize host,
  `https://system.netsuite.com/app/login/oauth2/authorize.nl`, "if you don't know the specific account ID". The
  successful callback then carries `code`, `state`, **`role`**, **`entity`** and **`company`** — and `company` **is** the
  NetSuite account ID. You have it in hand before the token exchange, which is the only leg that needs an
  account-specific host. Asking the customer to type their account ID is avoidable onboarding friction.
- The callback's `role` and `entity` tell you which role and which user authorized — which is precisely what §8 says
  determines whether your token can read anything.

Also true and easy to miss:

- **Sandbox and Release Preview are separate accounts.** An authorization made in production is not copied to them, and
  every sandbox refresh clears the authorizations that were there. A customer who tests in a sandbox and then goes live
  authorizes twice.
- **The realm value in error headers uses the underscore form**, not the hyphenated hostname form. A 401 from REST comes
  back as `WWW-Authenticate: Bearer realm="123456", error="invalid_token", ...`, where the realm is the account ID the
  data was requested for — useful for confirming you are talking to the account you think you are.

**Product fact.** As of 2026-09-20, the connector asks the customer for their NetSuite account ID up front (it labels it
"NetSuite account ID" and points at *Setup > Company > Company Information*, with `1234567_SB1` given as the sandbox
example), then substitutes the normalized form into the authorize host, the token host and the REST base URL per
connection — underscores to hyphens, trailing acronym lowercased. It accepts the hyphenated form too and converts it
back to underscores where NetSuite wants a realm. It also supports the older token-based path (account ID/realm plus a
consumer key, consumer secret, token ID and token secret) alongside OAuth 2.0. It does **not** appear to read the
account ID from the callback's `company` parameter or use the generic `system.netsuite.com` authorize host. Confirm the
current behaviour with the connector's owner.

## 8. Features and the role/permission model — why a valid token still fails

This is the part that generates support tickets, and none of it is visible from the integration record.

### Features that must be enabled, in the customer's account

- **OAuth 2.0** — *Setup > Company > Enable Features > SuiteCloud* subtab, **Manage Authentication** section, check
  **OAuth 2.0**, accept the SuiteCloud Terms of Service, Save. If this is off, the authorization fails with
  `FeatureDisabled` — and the *Manage Integrations* menu may not appear at all, which reads as "NetSuite changed the UI"
  rather than "a feature is off".
- **REST Web Services** — *Setup > Company > Setup Tasks > Enable Features > SuiteCloud*, in the **SuiteTalk (Web
  Services)** section, with the SuiteCloud Terms of Service accepted. Without it, OAuth succeeds and the REST calls do
  not.
- **SuiteAnalytics Workbook** — *Enable Features > Analytics*. Oracle lists it among the prerequisites for REST web
  services.
- **Client SuiteScript and Server SuiteScript** — both required if you will use OAuth 2.0 for **RESTlets**. Not needed
  for REST web services alone.
- **Token-based Authentication** is a *different* feature, for the older TBA/OAuth 1.0 path. Oracle's "Enable the
  OAuth 2.0 Feature" steps do not list it as a prerequisite for OAuth 2.0, so do not tell a customer to turn it on for
  an OAuth 2.0 connector unless the connector actually uses TBA.

A missing feature does not say "enable this feature" at the point of failure — you get `FeatureDisabled` in the audit
trail, or an ordinary-looking 401/403 at the first API call. When a customer's brand-new connection reads nothing, check
features before you check your code.

### Roles and permissions

The token is granted **for the role the user chose on the consent screen** (the consent screen even offers a *Choose
Another Role* link, and the callback returns the chosen `role`). Everything the token can do is what that role can do.

| Permission | Why it matters |
| --- | --- |
| **Log in using OAuth 2.0 Access Tokens** | Required to use OAuth 2.0 at all for REST web services, RESTlets and SuiteAnalytics Connect |
| **REST Web Services** (level Full) | The API permission. **Separate from any record permission** — a role can have full access to customers and still be unable to call REST |
| **Log in using Access Tokens** | Listed by Oracle among the required permissions for REST web services |
| **SuiteAnalytics Workbook** (level Edit) | Listed among the required permissions for REST web services |
| **SuiteAnalytics Connect** or **SuiteAnalytics Connect – Read All** | Only for the `suite_analytics` scope. "Read All" is read access to *all* NetSuite data including employee and customer records — Oracle flags the exposure explicitly |
| **OAuth 2.0 Authorized Applications Management** | Administrative only. Oracle states a role with this permission **cannot** access REST web services, RESTlets or SuiteAnalytics Connect using OAuth 2.0 — a genuine trap if someone hands you the "OAuth admin" role to authorize with. It also requires 2FA |

Two more facts that shape the ask you make of customers:

- **API permission and record permission are separate layers.** Oracle is explicit that record-level permissions are
  *additionally* required based on the operation — the example given is needing **Find Transaction** to retrieve a
  record with GET, and subsidiary restrictions when working with employees. A token that lists customers fine and 403s
  on transactions is a record-permission problem, not a scope problem, and no amount of re-authorizing fixes it.
- **Standard roles mostly already work.** Oracle lists roughly three dozen standard roles that ship with the REST Web
  Services, Log in using Access Tokens and SuiteAnalytics Workbook permissions — Accountant, Bookkeeper, A/P Clerk, A/R
  Clerk, CFO, System Administrator, Developer and so on. Custom roles are where this breaks, and Oracle explicitly
  recommends **not** using the Administrator role for REST web services.
- **2FA is not a blocker for the API, but it shapes who can authorize.** Administrator and other highly privileged roles
  are 2FA-required by default and the requirement cannot be removed; Oracle's guidance is that 2FA is not compatible
  with web services and that you should use OAuth 2.0 or TBA for those roles. The consent flow is an interactive login,
  so the authorizing user will be taken through their normal 2FA. Tell customers to authorize with a purpose-built
  integration role that carries the permissions above rather than with Administrator.

Ask the customer for a **named role** to authorize with, and have them confirm it carries *Log in using OAuth 2.0 Access
Tokens*, *REST Web Services* (Full) and the record permissions for the objects you will sync. That one sentence in an
onboarding doc removes most NetSuite support tickets.

## 9. Token lifetimes — the seven-day problem

| Token | Lifetime | Rule |
| --- | --- | --- |
| authorization `code` | short-lived, single use | Reused or stale codes give `invalid_grant` |
| `access_token` | **60 minutes** (`expires_in` is always `3600`) | JWT, `PS256`-signed; the payload carries `sub` (role;entity), `aud` (application ID and client ID) and `scope` |
| `refresh_token`, confidential client | **seven days** | Oracle's documented refresh **response sample contains only `access_token`, `expires_in` and `token_type`** |
| `refresh_token`, public client | **two days by default, one-time use**, configurable 1–720 hours on the integration record, with a separate *Maximum Time For Token Rotation* (default 168 hours) | The refresh response *does* return a new refresh token |

Read that table again, because it is the opposite of what most vendors do.

1. **For a confidential client, Oracle documents no rotation and no extension.** The sentence that describes a refresh
   token coming back on refresh is scoped to public clients: *"If you use public clients with OAuth 2.0, the refresh
   token request returns an access and refresh token."* The sample confidential response has no `refresh_token` field,
   and the *Refresh Token Validity* field only appears on the record when **Public Client** is checked. The plain
   reading is that a confidential-client connection dies **seven days after the user authorized**, no matter how
   diligently you refresh, and the customer must go through consent again. Build the reconnect prompt; do not discover
   this on day eight. **Confirm it empirically on your test account** — it is the one claim here whose consequences are
   large enough to be worth measuring rather than trusting.
2. **A client that does persist a returned refresh token is still correct.** Store whatever comes back and fall back to
   the existing value when the response omits it — that is right for both client types and costs nothing.
3. **When the refresh token expires the token endpoint returns `invalid_grant`**, and Oracle's instruction is
   unambiguous: go back to Step One of the flow. There is no silent recovery.
4. **Public client is the documented way to get a longer window** — up to 720 hours (30 days) of refresh-token validity
   with rotation — at the cost of having no client secret and needing strict one-time-use handling. It is a real
   architectural choice, not a checkbox. Surface it; do not decide it alone.
5. **Revoking the refresh token invalidates its access tokens**, so a disconnect in your UI should call the revoke
   endpoint rather than just forgetting the row.

**Product fact.** As of 2026-09-20, the connector refreshes with `grant_type=refresh_token` over HTTP Basic and, when
NetSuite's response omits a refresh token, **keeps the existing one** — which is the correct handling for a confidential
client. It does not appear to compute or store a refresh-token expiry for NetSuite, so nothing in it anticipates the
seven-day wall. Raise this with the connector's owner: the reconnect prompt, not the refresh loop, is what keeps NetSuite
connections alive.

## 10. Governance, concurrency and quotas

NetSuite meters **the customer's account**, not your app — which is the opposite of the connection-ceiling model most
vendors use, and it means your customers' other integrations are competing with yours.

- **Concurrency is governed at the account level** and covers web services and RESTlet requests combined.
- **Base limits by service tier** — legacy tiers: Shared 5, Tier 3 = 2, Tier 2 = 10, Tier 1 / 1+ = 15, Tier 0 = 20.
  Tiers sold from June 2020: Standard 5, Premium 15, Enterprise 20, Ultimate 20.
- **Each SuiteCloud Plus license adds 10** concurrent requests. Ultimate plus five licenses is 20 + 50 = 70.
- **Development and partner accounts are fixed at 5** and do not scale with SuiteCloud Plus licenses.
- **A customer can cap you specifically.** *Setup > Integration > Integration Management > Integration Governance* lets
  an administrator allocate a **Concurrency Limit** to an individual integration record — yours included. Oracle's own
  best practice is a single account limit without per-integration limits, but the field exists and customers use it on
  "unmanaged external applications".
- **Exceeding it is a 429** — the REST error codes `CONCURRENCY_LIMIT_EXCEEDED` (429) and `USER_ERROR` (429, often
  paired with concurrency limits) are the ones to catch and back off on.
- **SuiteQL over REST caps at 100,000 results** across all pages unless SuiteAnalytics Connect is enabled in the account.

There is no per-app install cap, no certification gate, and no app-store review for an integration record — the
commercial gates NetSuite has are on the customer's licence, not yours.

## 11. Verify end-to-end

Testing in the account that owns the record hides the entire cross-account story. Test both.

1. Authorize **your own** account through the platform's real connect flow. Confirm the token response carries a refresh
   token and that one read call works.
2. Authorize an account **you do not own** — this is the test that proves §1. Afterwards, check *Setup > Integration >
   Manage Integrations* in that account and confirm your record appears there without anyone having created it.
3. Authorize with a **non-Administrator role** that a real customer would use, and confirm the reads you need still
   work (§8). If they do not, you have a permission list to give customers, which is the deliverable.
4. Confirm the **account ID handling** end to end with a sandbox account ID containing `_SB1` — authorize host, token
   host and REST host must all carry the `-sb1` form (§7).
5. Force a **refresh**, then force a **second** refresh. Then check what your client stored: if NetSuite returned no new
   refresh token, confirm the old one was kept, and note the original issue time so you know when the seven days run
   out (§9).
6. Call **revoke** and confirm subsequent API calls 401.

| Symptom | Cause |
| --- | --- |
| User clicks connect and lands on a NetSuite error page — no callback at all | `redirect_uri` does not match the integration record; NetSuite does **not** redirect in this case (§4) |
| `InvalidRedirectURI` in the audit trail | Same (§4) |
| `UnknownIntegration` / `unauthorized_client` at authorize | Wrong or unknown client ID (§6) |
| `InvalidState` | `state` missing, reused, or outside 22–1024 characters (§5) |
| `InvalidRequest` at authorize | Missing required parameter, or missing/malformed PKCE parameters (§5) |
| `UnsupportedResponseType` | `response_type` is not `code` (§5) |
| `invalid_scope` / `ScopeMismatched` | Scope string malformed, or not checked on the integration record (§5) |
| `FeatureDisabled` | The OAuth 2.0 feature is off in that account (§8) |
| `IntegrationBlocked` | The auto-installed record is **Waiting for Approval** or its State is not Enabled — the customer's *Require Approval during Auto-Installation* preference (§1) |
| `AuthorizationCodeGrantRequired` | The Authorization Code Grant box is not checked on the record (§3) |
| `AuthorizationExplicitlyDenied` / `error=access_denied` | User clicked Deny or Back on the consent screen |
| `invalid_client` (401) at the token exchange | Wrong client ID or secret, or the Basic header is malformed (§6) |
| `invalid_grant` (400) right after consent | Code expired, already used, `redirect_uri` differs from Step One, or `code_verifier` does not match `code_challenge` (§5) |
| `InvalidGrant` naming `code_verifier` | PKCE verifier mismatch between the two legs — check that the verifier is stored per flow and is stable across it (§5) |
| `ClientIdMismatch` | Different `client_id` used at Step One and Step Two (§6) |
| `invalid_grant` on a connection about a week old | Refresh token passed its seven days — the customer must re-authorize (§9) |
| `unsupported_grant_type` (400) | `grant_type` is neither `authorization_code` nor `refresh_token` |
| `EntityOrRoleDisabled` | The authorizing user or the role they chose is inactive (§8) |
| 401 `invalid_token` / "Invalid login attempt." | Access token expired, revoked, malformed or invalid — refresh, then re-authorize (§9) |
| `AccessTokenExpired` in the Login Audit Trail | Refresh, or restart the flow (§9) |
| `TokenRejected` / `InvalidTokenType` | A refresh token was sent as a bearer token, or vice versa (§9) |
| `InvalidIntegration` | The integration record does not exist in that account — auto-installation did not happen or was removed (§1) |
| 403 or a permission violation on a token that authorized cleanly | The authorizing role lacks **REST Web Services**, or lacks the record permission for that object — separate layers (§8) |
| Reads work for one object and not another | Record-level permission or subsidiary restriction on that object (§8) |
| 429 `CONCURRENCY_LIMIT_EXCEEDED` or `USER_ERROR` | Account-level concurrency governance, possibly a per-integration cap set by the customer (§10) |
| Everything worked, then the customer refreshed their sandbox | Sandbox refresh clears OAuth 2.0 authorizations and client-credentials setup (§7) |

NetSuite also records all of this: the **Login Audit Trail** shows failed OAuth 2.0 logins with the error name in the
**Detail** column. Ask a stuck customer for that row before theorising.

## 12. Hand off — never commit the secret

- **Do not** write the client secret into source control, a test, a fixture, a committed `.env`, a ticket, a PR body or
  a chat channel. Values go to the user, for the secret store or console.
- If a code change is needed (a redirect host, a scope box, account-ID normalization, a reconnect prompt), keep it
  secret-free and say what the human must set out of band.
- Close with: the integration record name; **which NetSuite account owns it** and who there can reset the secret; the
  client ID; where the secret was delivered; the grant type and whether it is a confidential or public client; the exact
  scope strings and the matching checked boxes; the redirect URIs added; the authorize, token and revoke URL patterns
  with `<accountID>` left as a placeholder; the refresh-token lifetime you are planning around; the feature and
  permission list customers must satisfy (§8); and anything left for the user to do.

## Stop and ask

Hand back to a human rather than guessing when: the owning NetSuite account has not been decided, or the only account
available is a sandbox; someone proposes asking every customer to create their own integration record (that is §1, and
it is the wrong shape — say so and show the auto-installation behaviour); the confidential-client seven-day refresh
window is incompatible with the product's reconnect story and someone must choose between re-consent prompts and a
public client; a customer's account has *Require Approval during Auto-Installation* set to True and needs an
administrator to enable your record; the roadmap needs RESTlets, SuiteAnalytics Connect or the `mcp` scope, each of
which changes the record's checkboxes and the customer's permission list; a customer wants the `SuiteAnalytics
Connect – Read All` permission scoped down, which is a data-exposure decision; resetting the client secret is on the
table (it breaks every existing connection); or the portal does not match the **Platform state** section above.

## References

- OAuth 2.0 (chapter overview) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_157769826287.html
- OAuth 2.0 Authorization Code Grant Flow — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158074210415.html
- Step One: GET Request to the Authorization Endpoint (parameters, scopes, state length, PKCE) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158081944642.html
- Step Two: POST Request to the Token Endpoint (Basic auth, token lifetimes) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158081952044.html
- Refresh Token POST Request to the Token Endpoint — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158082518856.html
- POST Request to the Revoke Token Endpoint — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_161071224088.html
- OAuth 2.0 Access and Refresh Token Structure — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158255317571.html
- Create Integration Records for Applications to Use OAuth 2.0 — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771733782.html
- Enabling an Application to Use OAuth 2.0 — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771863841.html
- Enable the OAuth 2.0 Feature — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771482304.html
- OAuth 2.0 Tasks for Administrators — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771171166.html
- Set Up OAuth 2.0 Roles — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771510070.html
- Managing OAuth 2.0 Authorized Applications — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_160855309644.html
- Viewing and Revoking OAuth 2.0 Authorized Applications — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157960925955.html
- OAuth 2.0 Client Credentials Setup (per-account, not auto-installed) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162686838198.html
- OAuth 2.0 for REST Web Services — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157780312610.html
- Integration Record Overview — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4389727047.html
- Distributing Integration Records — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4389835426.html
- Auto-Installation of Integration Records — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4429293934.html
- Troubleshooting OAuth 2.0 — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157780265265.html
- Authorization Code Grant Flow Errors — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158521803235.html
- Authorization Errors in Step One — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_158521823069.html
- Response Errors in Step Two and in the Refresh Token Response — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_158521832417.html
- Authorization Code Grant Flow Error Messages (audit-trail names) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_158711892221.html
- RESTlets and REST Web Services Authentication Errors — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_158521885412.html
- RESTlets and REST Web Services Error Messages in the Login Audit Trail — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_158324637123.html
- OAuth 2.0 and the Login Audit Trail — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_158323851171.html
- Prerequisites and Setup for REST Web Services (features, permissions, standard roles) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/article_5085602973.html
- Roles and Permission Considerations for APIs — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/article_0320025211.html
- Error Handling in REST Web Services (error body, error codes) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156570709583.html
- REST Web Services URL Schema and Account-Specific URLs — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1546938065.html
- Executing SuiteQL Queries Through REST Web Services — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157909186990.html
- Understanding Account IDs in URLs (underscore/hyphen rule) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1498754928.html
- URLs for Account-Specific Domains — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1498251763.html
- Web Services and RESTlet Concurrency Governance — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1500275531.html
- Concurrency Governance Limits Based on Service Tiers and SuiteCloud Plus Licenses — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/bridgehead_1500275603.html
- Concurrency Limit per Integration — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/bridgehead_156224824287.html
- Mandatory Two-Factor Authentication (2FA) for NetSuite Access — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1532968056.html
- Permissions Requiring Two-Factor Authentication (2FA) — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_1515446005.html
- Sandbox accounts — https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4609939018.html
