Skip to main content

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

Cloud ArchitecturePalaniappan P13 min read

Quick summary: 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.

Key Takeaways

  • 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
  • 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
  • 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
  • The data-pipeline side (cost and vendor data into AWS) is on the NetSuite integration with AWS page
Typographic data card, not a product screenshot. Headline: Amazon Connect plus NetSuite. Table of limits: access token 3600 seconds, certificate maximum 2 years, 5 active certificates per mapping, SuiteQL maximum 100,000 rows, Connect Lambda wait 8 seconds.
Table of Contents

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. The data-pipeline side (cost and vendor data into AWS) is on the NetSuite integration with 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.


What is native, what is custom

PieceLabel
Invoke AWS Lambda function block, contact attributes, Customer ProfilesNative (Connect)
NetSuite REST web services, SuiteQL, OAuth 2.0 client credentialsNative to NetSuite (Oracle)
The Lambda that signs the assertion, queries, and returns a string mapCustom engineering (code in erp/netsuite.mjs and erp/handler.mjs)
A normalized phone field, a saved search, a cached summary in Customer ProfilesCustom (you build and maintain it)
iPaaS connectorsThird-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):

# 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 partValue in the sampleStatus
header typ, alg, kidJWT, PS256 by default, Certificate IDalg list is Oracle-documented
issintegration client IDdocumented
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/tokenunverified: Oracle’s own sample uses a different host form (restlets in the host) for its example account, so test this in sandbox
iat, expnow, now plus 300 sOracle: 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:

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 keyNetSuite sourceNotes
lookupStatushandlerOK or FALLBACK. Branch here before reading anything else.
fallbackReasonhandlerTIMEOUT, HTTP_401, HTTP_429, AMBIGUOUS, BAD_RESPONSE, NOT_CONFIGURED, BAD_INPUT
accountStatuscustomer.isInactiveACTIVE, INACTIVE, NOT_FOUND. The sample compares the returned value to T; verify in your account.
openInvoiceCountexact COUNT(*)count or UNKNOWN
lastOrderStatus, lastOrderRefnewest SalesOrdstatus 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, and showing the result to agents is in the custom agent UI post. 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.

Troubleshooting

SymptomLikely causeWhat to do
HTTP 401, INVALID_LOGINOracle’s documented 401 code. Bad or expired token, role lacks the log-in-with-OAuth permission, or the mapping is wrongOracle points to Setup, Users/Roles, User Management, View Login Audit Trail. Read the audit entry first.
Token endpoint fails right after you rotated the certificateOld key or old kid in the Lambda; or the old mapping was revoked and no new one existsOracle: 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 endpointOracle’s wording for an expired access token on reuse; clock skew on iat is our guess, not an Oracle statementBuild a fresh assertion and request again.
401 at the query endpoint hours after deployCached token past its 3600 secondsThe sample clears the cache on 401. The next call gets a new token; the failing call has already fallen back.
429 CONCURRENCY_LIMIT_EXCEEDEDAccount concurrency used up by all integrationsLower Lambda reserved concurrency, agree a share, consider a SuiteCloud Plus license (price not published).
NOT_FOUND for a customer who existsStored phone format differs from E.164Normalized custom field (step 6).
BAD_RESPONSEHTTP 200 with no items array, such as an error bodyLog the status and shape (not the content) and read the NetSuite response in sandbox.
AMBIGUOUSTwo customer records share the numberIntended. 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 and the Connect Customer admin guide.
  • 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 and JD Edwards, and NetSuite licensing, which is not public.

Oracle pages read October 28, 2026: client credentials setup, integration records, certificate conditions, token request and response, SuiteQL through REST, REST error handling and SuiteCloud Plus concurrency.

Frequently asked questions

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.
Palaniappan P
Palaniappan P

AWS Cloud Architect & AI Expert

AWS-certified cloud architect and AI expert with deep expertise in cloud migrations, cost optimization, and generative AI on AWS.

AWS ArchitectureCloud MigrationGenAI on AWSCost OptimizationDevOps

Recommended Reading

Explore All Articles »