---
title: Connect + NetSuite (Oct 2026): M2M OAuth, step by step
description: As of October 28, 2026, Oracle documents a NetSuite OAuth 2.0 access token that lasts 3600 seconds with no refresh token, a certificate capped at 2 years, and a SuiteQL cap of 100,000 rows. Here is the Connect Lambda setup, step by step.
url: https://www.factualminds.com/blog/amazon-connect-netsuite-integration-2026/
datePublished: 2026-10-28T00:00:00.000Z
dateModified: 2026-10-28T00:00:00.000Z
author: palaniappan-p
category: Cloud Architecture
tags: amazon-connect, contact-center, aws, netsuite, suiteql, oauth2, erp, aws-lambda
---

# Connect + NetSuite (Oct 2026): M2M OAuth, step by step

> As of October 28, 2026, Oracle documents a NetSuite OAuth 2.0 access token that lasts 3600 seconds with no refresh token, a certificate capped at 2 years, and a SuiteQL cap of 100,000 rows. Here is the Connect Lambda setup, step by step.

On October 28, 2026, Oracle's documentation for NetSuite's client credentials flow says an access token lasts 3600 seconds, there is no refresh token, a certificate is valid for at most two years, and a SuiteQL query returns at most 100,000 rows. What follows sets up the NetSuite side of an Amazon Connect lookup: integration record, certificate mapping, role, query pattern, and a test plan to run before a caller does.

This is the single-platform deep dive. For the comparison across NetSuite, Odoo and JD Edwards, the shared 8-second Lambda budget and the fallback design, read [Amazon Connect ERP integration: NetSuite, Odoo and JD Edwards](https://www.factualminds.com/blog/amazon-connect-erp-integration-netsuite-odoo-jd-edwards-2026/). The data-pipeline side (cost and vendor data into AWS) is on the [NetSuite integration with AWS](https://www.factualminds.com/integrations/netsuite-aws/) page. Here the question is narrower: a caller dials, a Lambda asks NetSuite three small questions, and the flow branches.

We have not run the artifact against a live NetSuite account. Its tests mock all HTTP. Where Oracle's documentation did not settle a fact, this post says so. Broader Connect work is on [Amazon Connect consulting](https://www.factualminds.com/services/amazon-connect/).

---

## What is native, what is custom

| Piece | Label |
| --- | --- |
| Invoke AWS Lambda function block, contact attributes, Customer Profiles | **Native** (Connect) |
| NetSuite REST web services, SuiteQL, OAuth 2.0 client credentials | **Native to NetSuite** (Oracle) |
| The Lambda that signs the assertion, queries, and returns a string map | **Custom engineering** (code in [`erp/netsuite.mjs`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/netsuite.mjs) and [`erp/handler.mjs`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/erp/handler.mjs)) |
| A normalized phone field, a saved search, a cached summary in Customer Profiles | **Custom** (you build and maintain it) |
| iPaaS connectors | **Third-party**, not evaluated |

We found no AWS-provided NetSuite connector for Connect.

## Step 1: Features and permissions

What Oracle's pages state, and what they leave to you:

- Integration record permissions. Oracle says Administrators or users with the Integration Application permission create integration records. Administrators or users with the OAuth 2.0 Authorized Applications Management permission create and revoke the certificate mapping.
- Features. We found no Oracle page listing the feature checkboxes. REST Web Services and OAuth 2.0 under SuiteCloud features are the usual ones; treat the exact names as unverified.
- Role permissions. Third-party setup guides name REST Web Services and Log in using OAuth 2.0 Access Tokens on the integration role. We could not confirm those names on an Oracle page. Also unverified: the exact permission names SuiteQL needs for customer, invoice and sales order records. Start with view-only on those three record types and add only what a sandbox error asks for.

## Step 2: Create the integration record

Oracle's steps:

1. Setup, Integration, Manage Integrations, New.
2. Enter a name, set State to **Enabled**.
3. On the Authentication subtab, under OAuth 2.0, check **Client Credentials (Machine to Machine) Grant**.
4. Check the **REST Web Services** scope. The sample asks for `rest_webservices` and nothing else.
5. Save.

Oracle shows the client ID and client secret only once, on first save. Copy the client ID into Secrets Manager now; it is the JWT `iss` claim. Oracle states the M2M grant does not support public clients; a server-side Lambda is not one.

## Step 3: Generate the certificate and map it

Oracle's certificate page sets the rules: x.509 public certificate with extension `.cer`, `.pem` or `.crt`; RSA 3072 or 4096 bits, or EC 256, 384 or 521 bits; maximum validity two years (a longer period is shortened automatically); one certificate per unique combination of integration record, role and entity. Oracle emails Administrators about expiry at two months, one month and 14 days.

The RSA-PSS command from Oracle's page, for the sample's default `PS256` (Oracle's example uses 365 days; the maximum is two years):

```bash
# From Oracle's certificate page: RSA-PSS key and certificate. Sign with PS256.
openssl req -new -x509 -newkey rsa:4096 -keyout private.pem \
  -sigopt rsa_padding_mode:pss -sha256 -sigopt rsa_pss_saltlen:64 \
  -out public.pem -nodes -days 365
```

Keep `private.pem` out of source control and load it into Secrets Manager. Upload only `public.pem`. Then create the mapping:

1. Setup, Integration, Manage Authentication, OAuth 2.0 Client Credentials (M2M) Setup, Create New.
2. Choose the entity, the role and the application (the integration record from step 2). The application appears in the list only if the M2M box is checked on its record.
3. Upload the public certificate and Save.

NetSuite generates a Certificate ID. That value is the `kid` in the JWT header (`NS_CERT_ID` in the sample). Oracle allows at most five active certificates per combination of integration record, entity and role, which is what makes overlapping rotation possible: add the new certificate, switch the Lambda, then revoke the old one.

## Step 4: The token request

Oracle's token endpoint is `https://<accountID>.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token`. Send a form-encoded POST with `grant_type=client_credentials`, `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` and the signed `client_assertion`.

What the sample signs, mapped to Oracle's request-token structure:

| JWT part | Value in the sample | Status |
| --- | --- | --- |
| header `typ`, `alg`, `kid` | `JWT`, `PS256` by default, Certificate ID | `alg` list is Oracle-documented |
| `iss` | integration client ID | documented |
| `scope` | `["rest_webservices"]` | the Oracle sample uses an array; the reference page also allows a comma-separated string |
| `aud` | `${NS_BASE}/services/rest/auth/oauth2/v1/token` | **unverified**: Oracle's own sample uses a different host form (`restlets` in the host) for its example account, so test this in sandbox |
| `iat`, `exp` | now, now plus 300 s | Oracle: `exp` less than 60 minutes after `iat` |

One detail the docs do not settle: the salt length for RSA-PSS signing. The sample uses 32 bytes. If the token endpoint rejects a signature that you are sure is made with the right key, vary that first.

The response is an `access_token`, `expires_in` of 3600, and `token_type` bearer. The sample caches the token in module memory with a 30-second margin, and clears it when a query returns 401. A cold start requests a token immediately, one extra round trip inside your 8 seconds, which is why the handler aborts at 5 seconds.

## Step 5: The query pattern

Oracle documents SuiteQL over REST as a `POST` to `/services/rest/query/v1/suiteql`, query in the body as `q`, with the header `Prefer: transient`. Paging uses the `limit` and `offset` URL parameters, and the response shows `count`, `offset`, `totalResults` and `items`. A query returns at most 100,000 results. Oracle's page for this does not state the default or maximum `limit`; some third-party material mentions 1,000 and a `hasMore` flag, which we did not verify on an Oracle page. Do not build on those numbers.

The sample does not page at all, on purpose. A caller needs three small answers, not a result set. These are the three queries it runs per contact, after the phone is validated as E.164:

```sql
SELECT id, isInactive FROM customer WHERE phone = '+14155550123'            -- limit 2
SELECT COUNT(*) AS n FROM transaction
  WHERE entity = 4821 AND type = 'CustInvc' AND status = 'A'                 -- limit 1
SELECT status, tranId FROM transaction
  WHERE entity = 4821 AND type = 'SalesOrd' ORDER BY tranDate DESC           -- limit 1
```

Three choices in that pattern come from failure modes.

- Limit 2 on the customer query. Two rows means two customer records share the number. The adapter throws `AMBIGUOUS` and the flow does not read either account aloud. Picking one would disclose someone else's order.
- An exact `COUNT(*)` for open invoices. Fetching a page of invoices and counting `items` would be wrong the moment the count exceeds the page. The count query always returns one row with an exact `n`. `CustInvc` with status `A` is how the sample defines open. NetSuite status codes differ by record type and configuration, so verify the code in your account before trusting the number.
- Bound parameters are available. Oracle documents `?` placeholders with a `params` array. The sample instead validates the phone to `^\+[1-9][0-9]{6,14}$` before it enters the string, and a test feeds it an injection-shaped value. Bound parameters would be reasonable extra hardening; we have not made that change.

The newest-sales-order and open-invoice queries run in parallel after the customer is found, so one contact uses up to two concurrent NetSuite requests for a moment.

## Step 6: Phone matching without lying to the caller

NetSuite's phone field is free text. An agent typed `(415) 555-0123`, an import wrote `415-555-0123`, and Connect hands you `+14155550123` from the carrier. `WHERE phone = '+14155550123'` matches only the record that happens to store that exact string, and the miss looks like `NOT_FOUND`: a real customer treated as a stranger.

Options, in order of how much we would trust them:

1. A normalized custom field (a custom entity field you add, such as E.164 phone) kept by a SuiteScript user event or by whatever process loads customers. Query the custom field. Label: custom. This is the one we would build.
2. A saved search or lookup table that stores the normalized value. Also custom; slower to maintain, easier to inspect.
3. Normalizing inside SuiteQL by stripping punctuation from the stored value. Whether the functions you need are supported in your account is unverified, and the query would no longer be able to use a simple equality on a stored field. Test the cost before you ship it.
4. Skip phone as the key: ask the caller for an order number or account ID in the flow and look that up instead.

Shared numbers (a household, a switchboard) still produce `AMBIGUOUS`, by design.

## Step 7: Concurrency and the 429

Oracle's documentation says web services and RESTlet concurrency are governed per account, as a combined total, that the base limit depends on the service tier, that each SuiteCloud Plus license adds 10, and that the limit is lenient, so a brief overage is possible. We did not obtain the base numbers per tier from an Oracle page, and third-party sources disagree, so we give none.

For REST, Oracle's error page documents HTTP 429 with `o:errorCode` `CONCURRENCY_LIMIT_EXCEEDED`, the detail "Concurrent request limit exceeded. Request blocked.", and the remedy "retry". The sample adapter does not retry. In a voice flow an immediate second attempt competes with the next caller, and Connect already retries a failed invoke up to 3 times inside the 8 seconds. It reports `HTTP_429` as the `fallbackReason`.

> **What could break (modeled, not an incident)** — Assume your NetSuite account has an unknown limit of N concurrent requests, and a marketing campaign sends a burst of callers. Each contact uses one request, then two in parallel, so 10 simultaneous contacts can ask for 20. If other integrations (a nightly sync, a warehouse system) already hold most of N, some lookups return 429 and those callers get the generic path. They are not misrouted; they simply lose the personalization. Your burst can also push the other integrations over the limit.

Set reserved concurrency on the Lambda to a number you have agreed with whoever owns the NetSuite account. The Oracle pages we read described no per-integration allocation, so reserved concurrency is your control.

## Sandbox and production

The account ID is the subdomain: Oracle's examples show `https://123456.suitetalk.api.netsuite.com`. Sandbox accounts have their own account ID. We could not confirm the exact hostname format for sandbox from an Oracle page. A lowercase, hyphenated form such as `1234567-sb1` is commonly used, but treat that as unverified and copy the URL from your sandbox's own integration setup.

What Oracle does state matters more: the client credentials setup is not copied to other production accounts, Release Preview, sandbox or Next Preview accounts, and refreshing a sandbox clears it. After each refresh, redo the integration record check and the certificate mapping, and update `NS_CERT_ID` and `NS_CLIENT_ID` because both will differ per account. Keep one secret per environment.

## Mapping to Connect

The Lambda returns a flat string map. `lookupStatus` is the only key the flow should check first.

| Connect key | NetSuite source | Notes |
| --- | --- | --- |
| `lookupStatus` | handler | `OK` or `FALLBACK`. Branch here before reading anything else. |
| `fallbackReason` | handler | `TIMEOUT`, `HTTP_401`, `HTTP_429`, `AMBIGUOUS`, `BAD_RESPONSE`, `NOT_CONFIGURED`, `BAD_INPUT` |
| `accountStatus` | `customer.isInactive` | `ACTIVE`, `INACTIVE`, `NOT_FOUND`. The sample compares the returned value to `T`; verify in your account. |
| `openInvoiceCount` | exact `COUNT(*)` | count or `UNKNOWN` |
| `lastOrderStatus`, `lastOrderRef` | newest `SalesOrd` | status and `tranId`, values restricted to letters, digits, dash and underscore |

In the flow, use **Invoke AWS Lambda function** (synchronous), a timeout below 8 seconds, then **Set contact attributes** from the **External** namespace (`$.External.accountStatus` and so on), then **Check contact attributes** on `lookupStatus`. Anything in a contact attribute is stored on the contact record, so return only what the flow needs. Never return a NetSuite internal ID that the flow does not use.

For Customer Profiles, treat the NetSuite values as a cached summary, not a source of truth. If you want an agent workspace to show them, write a custom profile object keyed on the phone or the NetSuite ID from a scheduled job, and let the live lookup win when it succeeds. The profile mechanics are in [Customer Profiles in Connect](https://www.factualminds.com/blog/amazon-connect-customer-profiles-guide-2026/), and showing the result to agents is in the [custom agent UI post](https://www.factualminds.com/blog/amazon-connect-custom-agent-ui-customer-profiles-2026/). Doing both unthinkingly spends NetSuite concurrency twice.

## Test plan: run it in this order

1. Certificate and mapping, outside Lambda. Build the JWT with a library, call the token endpoint with `curl`, and confirm you receive an `access_token` and `expires_in` of 3600. Fix this before any code of yours is involved.
2. One SuiteQL call. `POST` `SELECT id FROM customer` with `limit=1` and the bearer token. A 200 with an `items` array proves the role can read customers.
3. Each query by hand. Run the three queries above for one known customer. Compare `n` with a saved search count.
4. Unit tests. From the `erp` folder run `node --test`. The 13 tests (mocked HTTP) cover the token request shape and reuse, the ambiguous and inactive paths, error bodies returned with HTTP 200, missing config, and a log line that never contains the phone. They prove the adapter's logic. They do not prove NetSuite accepts your signature.
5. Lambda in sandbox. Invoke with the sample event. Check the log line for `lookupStatus` and `ms`.
6. Flow test. Use Connect's testing and simulation on the flow, then place a real call from a number stored in the sandbox in three formats.
7. Failure drills. Revoke the test certificate (expect `HTTP_401` or a token failure, then fallback), pass an unknown number (expect `NOT_FOUND`), and put two customers on one number (expect `AMBIGUOUS`).
8. Alarm. Create a CloudWatch metric filter on `lookupStatus` `FALLBACK` by reason. The handler never throws, so Lambda's `Errors` metric stays at zero.

A reference checklist for the NetSuite side only is in [`netsuite-setup/README.md`](https://www.factualminds.com/examples/architecture-blog-2026/connect-commerce-context/netsuite-setup/README.md).

## Troubleshooting

| Symptom | Likely cause | What to do |
| --- | --- | --- |
| HTTP 401, `INVALID_LOGIN` | Oracle's documented 401 code. Bad or expired token, role lacks the log-in-with-OAuth permission, or the mapping is wrong | Oracle points to Setup, Users/Roles, User Management, View Login Audit Trail. Read the audit entry first. |
| Token endpoint fails right after you rotated the certificate | Old key or old `kid` in the Lambda; or the old mapping was revoked and no new one exists | Oracle: after revocation or expiry, create a new mapping. The new Certificate ID becomes the new `kid` (our inference). Update both secrets together. |
| `invalid_grant` from the token endpoint | Oracle's wording for an expired access token on reuse; clock skew on `iat` is our guess, not an Oracle statement | Build a fresh assertion and request again. |
| 401 at the query endpoint hours after deploy | Cached token past its 3600 seconds | The sample clears the cache on 401. The next call gets a new token; the failing call has already fallen back. |
| 429 `CONCURRENCY_LIMIT_EXCEEDED` | Account concurrency used up by all integrations | Lower Lambda reserved concurrency, agree a share, consider a SuiteCloud Plus license (price not published). |
| `NOT_FOUND` for a customer who exists | Stored phone format differs from E.164 | Normalized custom field (step 6). |
| `BAD_RESPONSE` | HTTP 200 with no `items` array, such as an error body | Log the status and shape (not the content) and read the NetSuite response in sandbox. |
| `AMBIGUOUS` | Two customer records share the number | Intended. Route to verification. |

## Security

- Least privilege. One integration role, view-only on the three record types, assigned to a dedicated entity. One certificate per integration-role-entity combination, as Oracle requires.
- Secrets. Client ID, Certificate ID and private key in Secrets Manager, read at cold start and cached. Never plain Lambda environment variables outside a test.
- PII. Contact attributes are stored on the contact record. The sample's log line carries status, reason and milliseconds, never the phone or ERP data.
- Compliance. Using AWS and NetSuite does not make this compliant by itself.

## Costs (published only)

- Amazon Connect Customer, checked October 28, 2026: voice $0.038 per minute, chat $0.010 per message, email $0.080, SMS and third-party messaging $0.014, telephony extra. Connect Customer includes AI agents, conversational analytics and summaries in those rates; Connect Customer Basic excludes them. Sources: [Connect pricing](https://aws.amazon.com/connect/pricing/) and the [Connect Customer admin guide](https://docs.aws.amazon.com/connect/latest/adminguide/enable-nextgeneration-amazonconnect.html).
- Lambda and Secrets Manager are metered; use their published pricing pages with your measured invocations. We state no totals.
- NetSuite: the license fee, and a SuiteCloud Plus license if you buy one, are not published. Oracle publishes that each SuiteCloud Plus license adds 10 to the concurrency limit, not the price.

## Recommendation

Build the live lookup only when three things hold: a measured p95 under about 3 seconds on the token-cached path, a concurrency share agreed in writing with the NetSuite owner, and a normalized phone field or a different key. The trade-off: a live lookup gives current data and costs account concurrency and failure surface; a cached summary is cheap and stale. If any of the three is missing, ship the cached summary first and keep live lookup for cases where staleness hurts, such as an order status the caller asks about right now.

## What to do this week

1. Create the integration record and mapping in a sandbox and obtain a token with `curl`.
2. Ask the NetSuite owner for the account's concurrency limit and who else uses it.
3. Decide the phone key: normalized custom field or ask-for-an-identifier.
4. Verify the open-invoice status code and the `isInactive` value in your account.
5. Run `node --test` in the artifact, then invoke the Lambda from sandbox.
6. Put the certificate expiry (Oracle emails at two months, one month and 14 days) in a calendar you own.

## What this post doesn't cover

- A run against live NetSuite. The tests are mocked.
- Oracle's feature-checkbox names and the exact SuiteQL permission names, which we could not confirm on an Oracle page.
- Per-tier concurrency numbers, the default and maximum SuiteQL `limit`, and the sandbox hostname format.
- NetSuite writes (creating cases, changing orders). The scope is read-only.
- Odoo and JD Edwards, covered in [Odoo](https://www.factualminds.com/blog/amazon-connect-odoo-integration-2026/) and [JD Edwards](https://www.factualminds.com/blog/amazon-connect-jd-edwards-integration-2026/), and NetSuite licensing, which is not public.

Oracle pages read October 28, 2026: [client credentials setup](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162686838198.html), [integration records](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157771733782.html), [certificate conditions](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/subsect_162755332391.html), [token request and response](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_162755359851.html), [SuiteQL through REST](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_157909186990.html), [REST error handling](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_156570709583.html) and [SuiteCloud Plus concurrency](https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_N3448861.html).

## FAQ

### How long does a NetSuite client credentials access token last?
Oracle documents expires_in as always 3600 seconds, and the response has no refresh token. When the token expires you build and sign a new assertion and call the token endpoint again. The sample adapter caches the token in the Lambda container and drops it on a 401 from the query endpoint.

### Which NetSuite signing algorithms and certificates work?
Oracle's request-token page lists PS256, PS384, PS512, ES256, ES384 and ES512 for the JWT header alg. Its certificate page allows RSA 3072 or 4096 bits and EC 256, 384 or 521 bits, in x.509 format (.cer, .pem or .crt), with a two-year maximum validity. RS256 is not on the list. The sample defaults to PS256 with an RSA key.

### Should we match callers to NetSuite customers by phone number?
Only as a convenience. NetSuite phone fields are free text, so +14155550123 will not match (415) 555-0123. Phone also fails for shared lines and for callers who ring from a number that is not on the customer record. Do not use phone as the only gate for anything that discloses balances or changes an order. Add a verification step in the flow first.

### What happens when NetSuite returns 429?
Oracle documents HTTP 429 with errorCode CONCURRENCY_LIMIT_EXCEEDED and says to retry. Inside a Connect flow you cannot afford a retry loop in an 8 second window, so the sample adapter reports fallbackReason HTTP_429 and the flow routes the caller to the generic path. The real fix is capping the Lambda's reserved concurrency below the share of the account limit you may use.

### When should we not do a live NetSuite lookup in the call flow?
When your measured p95 for three SuiteQL calls does not fit inside about 5 seconds, when the NetSuite account's concurrency budget is already used by other integrations, or when you do not yet have a role that is limited to read-only. In those cases use a cached summary kept in Customer Profiles and refresh it outside the call.

### What could go wrong after we rotate the certificate?
Oracle says that after a revocation or expiry you must create a new mapping before the integration can use the flow again. A new mapping produces a new Certificate ID, which is the kid in the JWT header. If the Lambda still holds the old key or the old kid, every token request fails and every call falls back. Rotate in Secrets Manager and the NetSuite mapping together, and test in sandbox first.

### Does the sandbox share the production OAuth setup?
No. Oracle states the client credentials setup is not copied to other production accounts, Release Preview, sandbox or Next Preview accounts, and that refreshing a sandbox clears it. Plan to redo the integration record check, the mapping and the certificate upload after every sandbox refresh, and use the sandbox account ID in the base URL.

### What does NetSuite licensing cost for this integration?
Not published. Oracle publishes that each SuiteCloud Plus license adds 10 to the account concurrency limit, but not the price. Get a quote from your NetSuite account team. The Amazon Connect side is published: Connect Customer lists $0.038 per voice minute plus telephony as of October 28, 2026.

---

*Source: https://www.factualminds.com/blog/amazon-connect-netsuite-integration-2026/*
