---
# === IDENTITY ===
id: business/erp-integration/oauth2-implementation-across-erps/2026
canonical_question: "How do OAuth 2.0 implementations differ across ERPs - Salesforce, SAP, Oracle, Workday, Dynamics 365, NetSuite?"
aliases:
  - "OAuth 2.0 grant types comparison ERP Salesforce SAP Oracle Workday D365"
  - "Which OAuth 2.0 flow for ERP server-to-server integration"
  - "ERP OAuth token lifetime refresh behavior comparison"
  - "JWT bearer vs client credentials for ERP API authentication"
entity_type: erp_integration
domain: business > erp-integration > oauth2-implementation-across-erps
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === SYSTEM PROFILE ===
systems:
  - name: "Salesforce"
    vendor: "Salesforce"
    version: "API v62.0"
    edition: "Enterprise, Unlimited, Developer"
    deployment: "cloud"
    api_surface: "REST, SOAP, Bulk, Streaming"
  - name: "SAP S/4HANA Cloud"
    vendor: "SAP"
    version: "2408"
    edition: "Public Cloud, Private Cloud"
    deployment: "cloud"
    api_surface: "OData v4, SOAP, REST"
  - name: "Oracle Fusion Cloud ERP"
    vendor: "Oracle"
    version: "24B"
    edition: "Enterprise"
    deployment: "cloud"
    api_surface: "REST, SOAP, FBDI"
  - name: "Oracle NetSuite"
    vendor: "Oracle"
    version: "2024.2+"
    edition: "SuiteCloud"
    deployment: "cloud"
    api_surface: "REST, SuiteTalk SOAP"
  - name: "Microsoft Dynamics 365"
    vendor: "Microsoft"
    version: "Dataverse Web API v9.2"
    edition: "Enterprise, Business Central"
    deployment: "cloud"
    api_surface: "OData v4, REST"
  - name: "Workday"
    vendor: "Workday"
    version: "v43.0+"
    edition: "Enterprise"
    deployment: "cloud"
    api_surface: "REST, SOAP (WWS)"

# === VERIFICATION ===
last_verified: 2026-03-07
confidence: 0.87
version: 1.0
first_published: 2026-03-07

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: volatile
  last_breaking_change: "NetSuite deprecating TBA (OAuth 1.0) for new integrations as of 2027.1; SAP deprecating basic auth for SuccessFactors by Nov 2026"
  next_review: 2026-09-03
  change_sensitivity: high

# === CONSTRAINTS ===
constraints:
  - "Salesforce JWT Bearer flow requires connected app with uploaded X.509 certificate; no refresh tokens issued"
  - "SAP S/4HANA Cloud Public Edition restricts to communication arrangements for OAuth; custom OAuth scopes not supported"
  - "Oracle Fusion Cloud requires OCI IAM (formerly IDCS) confidential app; basic auth deprecated for new integrations"
  - "NetSuite OAuth 2.0 requires SuiteCloud feature enabled; TBA still needed for SOAP SuiteTalk integrations"
  - "Dynamics 365 client secrets expire max 24 months; certificate auth recommended for production"
  - "Workday OAuth 2.0 requires ISU with Integration scope; non-expiring refresh tokens optional but recommended for unattended flows"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need broad authentication comparison (not just OAuth 2.0)"
    use_instead: "business/erp-integration/erp-authentication-comparison/2026"
  - condition: "Need Salesforce OAuth flows in detail (all 5 flows, code, troubleshooting)"
    use_instead: "business/erp-integration/salesforce-oauth-authentication/2026"
  - condition: "Need Dynamics 365 authentication deep-dive"
    use_instead: "business/erp-integration/dynamics-365-authentication/2026"
  - condition: "Need Workday ISU and OAuth setup walkthrough"
    use_instead: "business/erp-integration/workday-authentication-isu-oauth/2026"
  - condition: "Need SAP authentication methods (OAuth + X.509 + SAML)"
    use_instead: "business/erp-integration/sap-authentication-methods/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: erp_system
    question: "Which ERP system(s) are you integrating?"
    type: choice
    options:
      - "Salesforce"
      - "SAP S/4HANA"
      - "Oracle Fusion Cloud ERP"
      - "NetSuite"
      - "Dynamics 365"
      - "Workday"
      - "Multiple / cross-system"
  - key: integration_pattern
    question: "What kind of OAuth flow do you need?"
    type: choice
    options:
      - "server-to-server (no user interaction)"
      - "user-delegated (on behalf of user)"
      - "device flow (CLI or IoT)"
      - "not sure -- need guidance"
  - key: certificate_vs_secret
    question: "Do you want certificate-based or client-secret-based authentication?"
    type: choice
    options:
      - "certificate (X.509)"
      - "client secret"
      - "either -- need guidance"

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/business/erp-integration/oauth2-implementation-across-erps/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-03-07)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "business/erp-integration/erp-authentication-comparison/2026"
      label: "Broader ERP authentication comparison (OAuth + certs + service accounts)"
    - id: "business/erp-integration/salesforce-oauth-authentication/2026"
      label: "Salesforce OAuth deep-dive"
    - id: "business/erp-integration/dynamics-365-authentication/2026"
      label: "Dynamics 365 authentication deep-dive"
    - id: "business/erp-integration/workday-authentication-isu-oauth/2026"
      label: "Workday ISU + OAuth setup"
    - id: "business/erp-integration/sap-authentication-methods/2026"
      label: "SAP authentication methods"
    - id: "business/erp-integration/oracle-erp-cloud-authentication/2026"
      label: "Oracle ERP Cloud authentication"
  solves:
    - id: "business/erp-integration/netsuite-tba-vs-oauth2/2026"
      label: "NetSuite TBA vs OAuth 2.0 migration guide"
  alternative_to: []
  often_confused_with:
    - id: "business/erp-integration/erp-authentication-comparison/2026"
      label: "General auth comparison (covers non-OAuth methods too)"

# === SOURCES ===
sources:
  - id: src1
    title: "OAuth 2.0 JWT Bearer Flow for Server-to-Server Integration"
    author: Salesforce
    url: https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_jwt_flow.htm&type=5
    type: official_docs
    published: 2025-10-01
    reliability: authoritative
  - id: src2
    title: "OAuth with X.509 Client Certificates -- SAP BTP Connectivity"
    author: SAP
    url: https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/oauth-with-x-509-client-certificates
    type: official_docs
    published: 2025-06-15
    reliability: authoritative
  - id: src3
    title: "Configure OAuth Using Oracle IDCS or IAM -- Oracle Fusion Cloud"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/sales/faaps/Configure_OAuth_Using_Oracle_IDCS_or_IAM.html
    type: official_docs
    published: 2025-08-01
    reliability: authoritative
  - id: src4
    title: "OAuth 2.0 Client Credentials Flow -- Microsoft Identity Platform"
    author: Microsoft
    url: https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow
    type: official_docs
    published: 2026-01-30
    reliability: authoritative
  - id: src5
    title: "NetSuite OAuth 2.0"
    author: Oracle
    url: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/chapter_157769826287.html
    type: official_docs
    published: 2025-09-01
    reliability: authoritative
  - id: src6
    title: "Complete Guide to Workday REST API Integration and Security"
    author: Reco.ai
    url: https://www.reco.ai/hub/workday-rest-api-integration-security
    type: technical_blog
    published: 2025-07-15
    reliability: moderate_high
  - id: src7
    title: "OAuth 2.0 vs OAuth 1.0 for NetSuite in 2025"
    author: LST Consultancy
    url: https://lstconsultancy.com/oauth2-vs-oauth1-for-netsuite/
    type: technical_blog
    published: 2025-04-10
    reliability: moderate_high
  - id: src8
    title: "Why is OAuth still hard in 2026?"
    author: Nango
    url: https://nango.dev/blog/why-is-oauth-still-hard
    type: technical_blog
    published: 2026-01-20
    reliability: moderate
---

# OAuth 2.0 Implementation Across ERPs: Salesforce, SAP, Oracle, Dynamics 365, NetSuite, Workday

## TL;DR

- **Bottom line**: Every major ERP supports OAuth 2.0 but implementations vary wildly -- Salesforce uses JWT bearer with connected apps, SAP uses communication arrangements via BTP, Oracle requires OCI IAM confidential apps, Dynamics 365 uses Entra ID app registrations, NetSuite recently added OAuth 2.0 alongside legacy TBA, and Workday requires ISU-linked API clients.
- **Key limit**: Token lifetimes range from 1 hour (Dynamics 365) to session-based (Salesforce, 2h default) to non-expiring refresh tokens (Workday) -- each requires different token management strategies.
- **Watch out for**: Certificate rotation deadlines -- SAP XSUAA managed certs default to 7 days validity, Dynamics 365 client secrets max 24 months, Salesforce connected app certs must be manually rotated.
- **Best for**: Integration architects comparing OAuth 2.0 grant types, token behavior, and implementation complexity across ERP platforms to choose the right auth flow for cross-system integrations.
- **Authentication**: Use JWT bearer (Salesforce), client credentials via communication arrangement (SAP), confidential app client credentials (Oracle), Entra ID client credentials (D365), authorization code (NetSuite), or refresh token flow with ISU (Workday) for server-to-server.

## System Profile

This card compares OAuth 2.0 implementations across the six dominant cloud ERP platforms as of March 2026. Each vendor has taken a fundamentally different approach to OAuth 2.0 -- from Salesforce's self-contained connected app model to Microsoft's centralized Entra ID identity platform. This card focuses exclusively on OAuth 2.0 grant types, token mechanics, and implementation patterns. For non-OAuth authentication methods (SAML, certificates-only, basic auth), see the broader authentication comparison card.

| System | Role | OAuth 2.0 Provider | Primary Server-to-Server Flow |
|---|---|---|---|
| Salesforce | CRM/Platform | Built-in (connected apps) | JWT Bearer |
| SAP S/4HANA Cloud | ERP | SAP BTP / XSUAA | Client Credentials via Communication Arrangement |
| Oracle Fusion Cloud ERP | ERP | OCI IAM (formerly IDCS) | Client Credentials (confidential app) |
| NetSuite | ERP | Built-in | Authorization Code (machine-to-machine via refresh) |
| Dynamics 365 | ERP/CRM | Microsoft Entra ID | Client Credentials |
| Workday | HCM/Finance | Built-in (API clients) | Refresh Token (with ISU) |

## API Surfaces & Capabilities

Each ERP's OAuth 2.0 implementation determines which API surfaces you can access. Not all OAuth flows unlock all API surfaces.

| ERP | OAuth-Protected APIs | Non-OAuth APIs (legacy) | OAuth Required Since |
|---|---|---|---|
| Salesforce | REST, SOAP, Bulk, Streaming, Composite | Username-Password (deprecated) | Connected apps since 2013; JWT since 2016 |
| SAP S/4HANA Cloud | OData v4, REST, SOAP (via BTP) | Communication user + basic auth (deprecated) | 2020 (BTP mandatory for Public Cloud) |
| Oracle Fusion Cloud | REST, SOAP, BI Publisher, FBDI | Basic auth (deprecated for new integrations) | 2023 (IDCS/OCI IAM mandatory) |
| NetSuite | REST (SuiteTalk REST), RESTlets, SuiteAnalytics Connect | TBA (OAuth 1.0) for SuiteTalk SOAP | Optional; TBA deprecated for new integrations 2027.1 |
| Dynamics 365 | Dataverse Web API (OData v4), custom APIs | S2S with Azure AD (same mechanism) | Always required (Azure AD since inception) |
| Workday | REST API, SOAP (WWS via OAuth) | ISU with basic auth (still supported) | Optional; recommended since v35.0+ |

## Rate Limits & Quotas

OAuth token acquisition endpoints have their own rate limits separate from API rate limits.

### Token Endpoint Limits

| ERP | Token Endpoint Rate Limit | Token Request Method | Notes |
|---|---|---|---|
| Salesforce | 1,800 token requests/hour per connected app | POST to /services/oauth2/token | Shared across all flows for the connected app [src1] |
| SAP S/4HANA Cloud | Fair-use / throttled per subaccount | POST to XSUAA /oauth/token | No published hard limit; excessive requests return 429 [src2] |
| Oracle Fusion Cloud | 60 token requests/minute per app | POST to OCI IAM /oauth2/v1/token | Hard limit per confidential application [src3] |
| NetSuite | Included in API concurrency limits | POST to /services/rest/auth/oauth2/v1/token | Shares request budget with other API calls [src5] |
| Dynamics 365 | No published per-app token limit | POST to login.microsoftonline.com/{tenant}/oauth2/v2.0/token | Entra ID throttles at tenant level [src4] |
| Workday | Not published | POST to /ccx/oauth2/{tenant}/token | Governed by overall API throughput limits [src6] |

### Rolling / Daily Limits

| ERP | Daily API Limit (OAuth-authenticated) | Window | Edition Differences |
|---|---|---|---|
| Salesforce | 100,000 API calls (Enterprise), 5M (Unlimited) | 24h rolling | Developer edition: 15,000 [src1] |
| SAP S/4HANA Cloud | Fair-use / throttled | Per-request | No edition-based tiering; monitored per communication arrangement |
| Oracle Fusion Cloud | Throttled per service | Per-request | Burst limits vary by API surface [src3] |
| NetSuite | Concurrency-based (max 10-25 concurrent) | Per-request | SuiteCloud Plus adds concurrency [src5, src7] |
| Dynamics 365 | 6,000 requests/5 min per user, 60,000/5 min per org | 5-min window | Same for all editions [src4] |
| Workday | Not published; throttled | Per-request | Monitored by Workday ops team [src6] |

## Authentication

### OAuth 2.0 Grant Types by ERP

| Grant Type | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | D365 | Workday |
|---|---|---|---|---|---|---|
| Authorization Code | Yes | Yes (via BTP) | Yes | Yes | Yes | Yes |
| Client Credentials | No (use JWT instead) | Yes | Yes | No | Yes | No (use refresh token) |
| JWT Bearer | Yes (primary S2S) | Yes (X.509) | No | No | Yes (cert-based) | No |
| Refresh Token | Yes (web server flow) | No | Yes | Yes | No (client creds) | Yes (primary S2S) |
| Device Flow | Yes | No | No | No | Yes | No |
| SAML Bearer | Yes | Yes | Yes (SAML assertion) | No | No | No |
| Username-Password (ROPC) | Deprecated | No | No | No | Yes (not recommended) | No |

[src1, src2, src3, src4, src5, src6]

### Token Lifetime & Refresh Behavior

| ERP | Access Token Lifetime | Refresh Token Lifetime | Refresh Behavior | MFA Impact |
|---|---|---|---|---|
| Salesforce | Session timeout (default 2h, configurable) | Until revoked (web server flow) | New JWT per request for JWT bearer; refresh token for web server flow | MFA blocks username-password flow; JWT/web server unaffected [src1] |
| SAP S/4HANA Cloud | 12 minutes (XSUAA default) | Not issued (client credentials) | Must request new token; short TTL by design | N/A -- communication user, not human identity [src2] |
| Oracle Fusion Cloud | 1 hour (OCI IAM default, configurable) | 7 days (default, configurable up to 100 days) | Standard refresh; offline_access scope required | N/A for confidential apps [src3] |
| NetSuite | 60 minutes | 7 days (re-issued on each refresh) | Rolling refresh -- new refresh token with each access token request | N/A for machine integrations [src5, src7] |
| Dynamics 365 | 60-75 minutes | Not issued (client credentials flow) | Request new token using credentials; MSAL caches tokens automatically | N/A for app-only context [src4] |
| Workday | Short-lived (minutes) | Non-expiring (optional), or 14 days | Refresh returns new access + new refresh token | N/A -- ISU is system account [src6] |

### Scopes Comparison

| ERP | Scope Model | Example Scope | Granularity |
|---|---|---|---|
| Salesforce | Predefined scope strings | `api`, `refresh_token`, `full`, `chatter_api` | Coarse -- per API surface [src1] |
| SAP S/4HANA Cloud | Communication scenario scopes | Defined per communication arrangement | Fine -- per business scenario [src2] |
| Oracle Fusion Cloud | OCI IAM scopes on confidential app | `urn:opc:idm:__myscopes__` or custom scopes | Medium -- per application [src3] |
| NetSuite | REST/SuiteAnalytics scope | `rest_webservices`, `suite_analytics` | Coarse -- per API surface [src5] |
| Dynamics 365 | Entra ID `.default` scope | `https://org.crm.dynamics.com/.default` | Coarse -- all permissions granted to app registration [src4] |
| Workday | Domain security policies via ISU | Scopes implicit from ISU domain permissions | Fine -- per security domain [src6] |

### Authentication Gotchas

- **Salesforce**: JWT bearer issues a new access token per request; there is no token refresh. The connected app's X.509 certificate must be manually rotated -- there is no auto-rotation. Session timeout is configurable by admin, so never hardcode 2h. [src1]
- **SAP**: XSUAA-managed certificates default to only 7 days validity. You must explicitly set longer validity (up to 1 year) when creating the service key. The token endpoint URL differs for mTLS: `https://{subdomain}.authentication.cert.{landscape}/oauth/token`. [src2]
- **Oracle**: IDCS was merged into OCI IAM in 2023. Old documentation references IDCS URLs; new integrations must use OCI IAM domain URLs. Two confidential applications are needed for Fusion Apps integration via OIC. [src3]
- **NetSuite**: OAuth 2.0 and TBA (OAuth 1.0) are different features in SuiteCloud. TBA is being deprecated for new integrations as of 2027.1. OAuth 2.0 does not require signature-based requests (unlike TBA). [src5, src7]
- **Dynamics 365**: The `.default` scope grants all permissions configured on the app registration -- you cannot request a subset at token time. Client secrets max out at 24 months and require proactive rotation. [src4]
- **Workday**: Refresh tokens are ISU-scoped. If the ISU is terminated or its security policies change, all OAuth tokens for that ISU become invalid immediately. [src6]

## Constraints
<!-- Agents: read this section before recommending any integration approach from this unit.
     These are hard constraints that must not be violated. -->

- Salesforce JWT bearer flow cannot be used without a connected app -- there is no "quick" OAuth path. The connected app requires a digital certificate (self-signed for dev, CA-signed recommended for prod). [src1]
- SAP S/4HANA Cloud Public Edition does not support custom OAuth scopes. Scopes are defined by communication scenarios in the SAP API Business Hub. You cannot create ad-hoc OAuth grants. [src2]
- Oracle Fusion Cloud basic auth was deprecated for new integrations in 2023. All new integrations must use OAuth 2.0 via OCI IAM. Existing basic auth integrations continue to work but will be sunset. [src3]
- NetSuite cannot use OAuth 2.0 for SuiteTalk SOAP. TBA (OAuth 1.0) remains the only option for SOAP integrations. OAuth 2.0 works only for REST web services, RESTlets, and SuiteAnalytics Connect. [src5]
- Dynamics 365 client credentials flow does not issue refresh tokens. You must request a new access token when the current one expires (typically 60-75 minutes). MSAL libraries handle this automatically. [src4]
- Workday OAuth 2.0 does not support client credentials grant. Server-to-server integrations use the refresh token flow linked to an ISU account. Without a valid refresh token, there is no way to obtain an access token without user interaction. [src6]

## Integration Pattern Decision Tree

```
START -- Choose OAuth 2.0 flow for ERP integration
|
+-- Is this server-to-server (no user interaction)?
|   |
|   +-- Salesforce?
|   |   +-- YES -> JWT Bearer Flow (connected app + X.509 cert)
|   |   +-- Need user context? -> Web Server Flow (authorization code + refresh token)
|   |
|   +-- SAP S/4HANA Cloud?
|   |   +-- Public Cloud -> Client Credentials via Communication Arrangement
|   |   +-- Private Cloud / On-Premise -> X.509 certificate or OAuth client credentials via BTP
|   |
|   +-- Oracle Fusion Cloud?
|   |   +-- YES -> Client Credentials (confidential app in OCI IAM)
|   |   +-- Via OIC middleware? -> Configure 2 confidential apps (trigger + invoke)
|   |
|   +-- NetSuite?
|   |   +-- REST APIs -> OAuth 2.0 authorization code + refresh token
|   |   +-- SOAP SuiteTalk -> TBA (OAuth 1.0) -- no OAuth 2.0 option
|   |
|   +-- Dynamics 365?
|   |   +-- YES -> Client Credentials (Entra ID app registration)
|   |   +-- Prefer cert over secret? -> Upload certificate to app registration
|   |
|   +-- Workday?
|       +-- YES -> Register API Client + ISU + Generate Refresh Token
|       +-- Use non-expiring refresh token for unattended integrations
|
+-- Is this user-delegated (on behalf of user)?
|   +-- Any ERP -> Authorization Code Flow
|   +-- Salesforce -> Web Server Flow (with callback URL)
|   +-- D365 -> Authorization Code + PKCE (for public clients)
|
+-- Is this a CLI or device?
    +-- Salesforce -> Device Flow
    +-- Dynamics 365 -> Device Code Flow
    +-- Others -> Not supported; use authorization code with browser redirect
```

## Quick Reference

### Cross-System OAuth 2.0 Implementation Summary

| Capability | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | Dynamics 365 | Workday |
|---|---|---|---|---|---|---|
| OAuth 2.0 Provider | Built-in (Connected Apps) | SAP BTP / XSUAA | OCI IAM (ex-IDCS) | Built-in | Microsoft Entra ID | Built-in (API Clients) |
| Primary S2S Grant | JWT Bearer | Client Credentials | Client Credentials | Auth Code + Refresh | Client Credentials | Refresh Token |
| Certificate Auth | X.509 on connected app | X.509 via mTLS | X.509 assertion | Not supported | Certificate on app registration | Not supported |
| Client Secret Auth | Yes (web server flow) | Yes (service key) | Yes (confidential app) | Yes (auth code flow) | Yes (app registration) | Yes (API client) |
| Token Lifetime | Session (~2h) | 12 min (XSUAA) | 1h (configurable) | 60 min | 60-75 min | Minutes |
| Refresh Token | Web server flow only | No (client creds) | Yes (7d default) | Yes (7d rolling) | No (client creds) | Yes (non-expiring opt.) |
| Scope Model | Coarse (api, full) | Per-scenario | Per-app / custom | Per-API-surface | .default (all-or-nothing) | ISU security domains |
| MFA Impact | Blocks ROPC only | N/A | N/A | N/A | N/A (app-only) | N/A (ISU) |
| Admin Portal | Setup > Apps > Connected | BTP Cockpit | OCI Console > Identity | Setup > Integration > OAuth 2.0 | Entra Admin Center | Workday > API Client |
| SDK/Library | jsforce, simple-salesforce | @sap/xsenv, SAP Cloud SDK | OCI SDK | N/A (standard HTTP) | MSAL (msal-node, msal4j) | N/A (standard HTTP) |

## Step-by-Step Integration Guide

### 1. Salesforce: Obtain Token via JWT Bearer Flow

Create a connected app in Salesforce Setup with a digital certificate, then sign a JWT assertion and exchange it for an access token. [src1]

```python
# Input:  Connected app consumer key, X.509 private key PEM, Salesforce username
# Output: Access token and instance_url

import jwt
import requests
import time

CONSUMER_KEY = "3MVG9..."
PRIVATE_KEY = open("server.key", "r").read()
USERNAME = "integration@company.com"
LOGIN_URL = "https://login.salesforce.com"

# Build JWT claim
claim = {
    "iss": CONSUMER_KEY,
    "sub": USERNAME,
    "aud": LOGIN_URL,
    "exp": int(time.time()) + 300  # 5-minute expiry
}
assertion = jwt.encode(claim, PRIVATE_KEY, algorithm="RS256")

# Exchange JWT for access token
response = requests.post(f"{LOGIN_URL}/services/oauth2/token", data={
    "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
    "assertion": assertion
})
token_data = response.json()
access_token = token_data["access_token"]
instance_url = token_data["instance_url"]  # CRITICAL: use this for all subsequent API calls
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" {instance_url}/services/data/v62.0/limits` -> returns JSON with API limits

### 2. SAP S/4HANA Cloud: Obtain Token via Client Credentials

Configure a communication arrangement in SAP, then request a token from the XSUAA token endpoint using the service key credentials. [src2]

```python
# Input:  XSUAA clientid, clientsecret, token URL from service key
# Output: Access token (12-minute TTL)

import requests

CLIENT_ID = "sb-comm-arrangement-xxx"
CLIENT_SECRET = "xxx..."
TOKEN_URL = "https://mysubaccount.authentication.eu10.hana.ondemand.com/oauth/token"

response = requests.post(TOKEN_URL, data={
    "grant_type": "client_credentials",
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET
})
access_token = response.json()["access_token"]
# Token expires in ~720 seconds (12 minutes) -- cache and refresh proactively
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" https://{s4hana-host}/sap/opu/odata4/.../$metadata` -> returns OData metadata XML

### 3. Oracle Fusion Cloud: Obtain Token via OCI IAM Client Credentials

Register a confidential application in OCI IAM, configure scopes, then request a token. [src3]

```python
# Input:  OCI IAM client_id, client_secret, token endpoint
# Output: Access token (1-hour default)

import requests

CLIENT_ID = "oci-app-client-id"
CLIENT_SECRET = "oci-app-client-secret"
TOKEN_URL = "https://idcs-xxx.identity.oraclecloud.com/oauth2/v1/token"

response = requests.post(TOKEN_URL, data={
    "grant_type": "client_credentials",
    "scope": "urn:opc:idm:__myscopes__"
}, auth=(CLIENT_ID, CLIENT_SECRET))

access_token = response.json()["access_token"]
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" https://{fa-host}/fscmRestApi/resources/11.13.18.05/invoices?limit=1` -> returns JSON invoice data

### 4. Dynamics 365: Obtain Token via Entra ID Client Credentials

Register an application in Microsoft Entra ID, grant Dynamics 365 API permissions, then request a token. [src4]

```python
# Input:  Entra ID tenant_id, client_id, client_secret, D365 org URL
# Output: Access token (60-75 minute lifetime)

import requests

TENANT_ID = "aaaabbbb-0000-cccc-1111-dddd2222eeee"
CLIENT_ID = "11112222-bbbb-3333-cccc-4444dddd5555"
CLIENT_SECRET = "A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u"
D365_URL = "https://myorg.crm.dynamics.com"

response = requests.post(
    f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token",
    data={
        "grant_type": "client_credentials",
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "scope": f"{D365_URL}/.default"
    }
)
access_token = response.json()["access_token"]
# No refresh token -- request a new token when expired
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" https://myorg.crm.dynamics.com/api/data/v9.2/WhoAmI` -> returns caller's system user ID

### 5. NetSuite: Obtain Token via OAuth 2.0 Authorization Code

Enable OAuth 2.0 in SuiteCloud, create an integration record, perform one-time authorization code exchange, then use refresh tokens for ongoing access. [src5]

```python
# Input:  client_id, client_secret, refresh_token (from initial auth code exchange)
# Output: New access token + new refresh token (rolling refresh)

import requests

CLIENT_ID = "netsuite-integration-client-id"
CLIENT_SECRET = "netsuite-client-secret"
REFRESH_TOKEN = "current-refresh-token"
ACCOUNT_ID = "1234567"
TOKEN_URL = f"https://{ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token"

response = requests.post(TOKEN_URL, data={
    "grant_type": "refresh_token",
    "refresh_token": REFRESH_TOKEN,
    "client_id": CLIENT_ID,
    "client_secret": CLIENT_SECRET
})
data = response.json()
access_token = data["access_token"]
new_refresh_token = data["refresh_token"]  # MUST store -- old refresh token is now invalid
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" https://{account_id}.suitetalk.api.netsuite.com/services/rest/record/v1/customer?limit=1` -> returns customer record JSON

### 6. Workday: Obtain Token via Refresh Token Flow

Register an API client in Workday, link it to an ISU, generate a non-expiring refresh token, then exchange for access tokens. [src6]

```python
# Input:  client_id, client_secret, refresh_token, Workday tenant
# Output: Access token + new refresh token

import requests

CLIENT_ID = "workday-api-client-id"
CLIENT_SECRET = "workday-api-client-secret"
REFRESH_TOKEN = "current-refresh-token"
TENANT = "mycompany"

response = requests.post(
    f"https://wd2-impl-services1.workday.com/ccx/oauth2/{TENANT}/token",
    data={
        "grant_type": "refresh_token",
        "refresh_token": REFRESH_TOKEN,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    }
)
data = response.json()
access_token = data["access_token"]
new_refresh_token = data["refresh_token"]  # Store securely for next exchange
```

**Verify**: `curl -H "Authorization: Bearer {access_token}" https://wd2-impl-services1.workday.com/ccx/api/v1/{tenant}/workers?limit=1` -> returns worker data

## Code Examples

### cURL: Token Acquisition for Each ERP

```bash
# --- Salesforce JWT Bearer ---
# Input:  JWT assertion (pre-generated), login URL
# Output: {"access_token":"...", "instance_url":"..."}

curl -X POST https://login.salesforce.com/services/oauth2/token \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
  -d "assertion=${JWT_ASSERTION}"

# --- SAP S/4HANA Client Credentials ---
# Input:  XSUAA clientid/secret, token URL
# Output: {"access_token":"...", "expires_in":720}

curl -X POST "${XSUAA_TOKEN_URL}" \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d "grant_type=client_credentials"

# --- Oracle Fusion Cloud Client Credentials ---
# Input:  OCI IAM client_id/secret, IDCS token URL
# Output: {"access_token":"...", "expires_in":3600}

curl -X POST "https://idcs-xxx.identity.oraclecloud.com/oauth2/v1/token" \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d "grant_type=client_credentials" \
  -d "scope=urn:opc:idm:__myscopes__"

# --- Dynamics 365 Client Credentials ---
# Input:  Entra tenant, client_id/secret, D365 org URL
# Output: {"access_token":"...", "expires_in":3599}

curl -X POST "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
  -d "grant_type=client_credentials" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}" \
  -d "scope=${D365_URL}/.default"

# --- NetSuite OAuth 2.0 Refresh ---
# Input:  client_id/secret, refresh_token
# Output: {"access_token":"...", "refresh_token":"NEW_TOKEN", "expires_in":3600}

curl -X POST "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=${REFRESH_TOKEN}" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}"

# --- Workday Refresh Token ---
# Input:  client_id/secret, refresh_token, tenant
# Output: {"access_token":"...", "refresh_token":"NEW_TOKEN"}

curl -X POST "https://wd2-impl-services1.workday.com/ccx/oauth2/${TENANT}/token" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=${REFRESH_TOKEN}" \
  -d "client_id=${CLIENT_ID}" \
  -d "client_secret=${CLIENT_SECRET}"
```

## Error Handling & Failure Points

### Common Error Codes

| Error Code | ERP(s) | Meaning | Cause | Resolution |
|---|---|---|---|---|
| `invalid_grant` | All | Token/assertion rejected | Expired JWT, revoked refresh token, or wrong credentials | Regenerate assertion or refresh token; check certificate validity [src1, src4] |
| `invalid_client` | All | Client ID/secret wrong | Misconfigured client credentials | Verify client_id and client_secret match the registered application |
| `unauthorized_client` | Salesforce, D365 | App not authorized for this grant type | Connected app not configured for JWT; Entra app not consented | Enable the specific OAuth flow in app configuration [src1, src4] |
| `invalid_scope` | Oracle, D365 | Requested scope not allowed | Scope not configured on confidential app or app registration | Add scope in OCI IAM console or Entra admin center [src3, src4] |
| 429 | SAP, Oracle | Rate limit exceeded on token endpoint | Too many token requests | Cache tokens; implement exponential backoff [src2, src3] |
| `INVALID_LOGIN` | Salesforce | JWT subject user doesn't exist or is inactive | Wrong username in JWT `sub` claim | Verify the integration user is active and has API access [src1] |
| `expired_token` | NetSuite, Workday | Refresh token expired | Token not refreshed within validity window (7d NetSuite, 14d Workday if not non-expiring) | Re-authorize via authorization code flow [src5, src6] |

### Failure Points in Production

- **Certificate expiry silently breaks integrations**: Salesforce connected app certs and SAP XSUAA managed certs (7-day default!) expire without warning. Fix: `Implement certificate rotation monitoring with alerts at 30, 7, and 1 day before expiry`. [src1, src2]
- **Refresh token rotation lost in transit**: NetSuite and Workday issue a new refresh token with each access token request. If you fail to store the new refresh token (e.g., crash between token response and database write), the old token is already invalid. Fix: `Use transactional storage -- write new refresh token before using the access token`. [src5, src6]
- **SAP token TTL too short for long-running jobs**: SAP XSUAA tokens expire after 12 minutes. A bulk data migration job running 30+ minutes will fail mid-execution. Fix: `Proactively refresh the token before each batch, or implement token refresh in the API client middleware`. [src2]
- **Dynamics 365 client secret forgotten rotation**: Client secrets expire silently after their configured lifetime (max 24 months). Fix: `Use certificate-based auth for production, or implement secret rotation automation with Azure Key Vault`. [src4]
- **Oracle IDCS-to-OCI IAM migration breaks token URLs**: The IDCS token endpoint URL format changed when Oracle merged IDCS into OCI IAM. Integrations using old `https://idcs-xxx.identity.oraclecloud.com` URLs may break when tenants are migrated. Fix: `Use the OCI IAM domain URL format and test during migration window`. [src3]
- **Workday ISU termination cascading failure**: If an ISU linked to OAuth API clients is terminated (e.g., offboarding mistake), all OAuth tokens for that ISU immediately become invalid. Fix: `Never link ISUs to human employees; create dedicated service ISUs with separate lifecycle management`. [src6]

## Anti-Patterns

### Wrong: Storing OAuth tokens in plaintext configuration files

```python
# BAD -- tokens and secrets in plaintext .env or config.json
config = {
    "salesforce_token": "00D5e000000XXXX!AR...",
    "sap_client_secret": "xsuaa-secret-value-here",
    "d365_client_secret": "A1bC2dE3f...",
    "workday_refresh_token": "wr-refresh-token..."
}
# Committed to Git, visible in CI logs, no rotation tracking
```

### Correct: Use a secrets manager with rotation support

```python
# GOOD -- secrets from vault with automatic rotation tracking
import boto3  # or azure.keyvault, google.cloud.secretmanager, hashicorp vault

secrets_client = boto3.client("secretsmanager")

def get_secret(secret_name):
    response = secrets_client.get_secret_value(SecretId=secret_name)
    return response["SecretString"]

salesforce_key = get_secret("erp/salesforce/private_key")
sap_secret = get_secret("erp/sap/client_secret")
d365_secret = get_secret("erp/d365/client_secret")
# Rotation policies enforced at the vault level
```

### Wrong: Requesting a new token for every single API call

```python
# BAD -- token request per API call wastes quota and adds latency
def get_salesforce_data(query):
    token = get_new_jwt_token()  # ~200ms network round trip each time
    return requests.get(f"{instance_url}/services/data/v62.0/query",
                       params={"q": query},
                       headers={"Authorization": f"Bearer {token}"})
```

### Correct: Cache tokens and refresh proactively before expiry

```python
# GOOD -- cache token, refresh when nearing expiry
import time

class TokenCache:
    def __init__(self):
        self._token = None
        self._expires_at = 0

    def get_token(self):
        # Refresh 60 seconds before actual expiry to avoid race conditions
        if time.time() > (self._expires_at - 60):
            self._token, self._expires_at = self._fetch_new_token()
        return self._token

    def _fetch_new_token(self):
        response = requests.post(TOKEN_URL, data={...})
        data = response.json()
        expires_at = time.time() + data["expires_in"]
        return data["access_token"], expires_at
```

### Wrong: Hardcoding ERP instance URLs with token endpoints

```python
# BAD -- hardcoded URLs break across environments
TOKEN_URL = "https://login.salesforce.com/services/oauth2/token"
API_URL = "https://na139.salesforce.com/services/data/v62.0/query"
# Fails when org moves to different pod, or when using sandbox
```

### Correct: Parse instance_url from token response; use environment config

```python
# GOOD -- dynamic instance URL from OAuth response
response = requests.post(login_url + "/services/oauth2/token", data={...})
data = response.json()
instance_url = data["instance_url"]  # Salesforce tells you where to call
# For SAP/Oracle/D365: use environment variables per deployment target
```

## Common Pitfalls

- **Assuming all ERPs support client credentials grant**: Salesforce and Workday do not. Salesforce uses JWT bearer instead; Workday uses refresh token flow with ISU. Building a generic OAuth client that assumes client_credentials breaks for 2 out of 6 major ERPs. Fix: `Abstract token acquisition per ERP; use adapter pattern for each vendor's flow`. [src1, src6]
- **Not storing Salesforce instance_url**: Salesforce returns `instance_url` in the token response. This URL can change (pod migrations). Always use the returned `instance_url`, never a hardcoded URL. Fix: `Store and use instance_url from each token response`. [src1]
- **Ignoring SAP's 12-minute token TTL**: SAP XSUAA tokens expire much faster than other ERPs. Middleware that caches tokens for 1 hour (common assumption) will fail 80% of the time with SAP. Fix: `Always respect the expires_in value from the token response; never assume a default TTL`. [src2]
- **Using Dynamics 365 client secrets in production**: Client secrets expire (max 24 months) and provide weaker security than certificates. Fix: `Use certificate-based authentication (client_assertion) for all production D365 integrations; secrets for dev only`. [src4]
- **Losing NetSuite rolling refresh tokens**: Each refresh returns a new refresh token, invalidating the old one. If you don't persist the new token, you lose access permanently. Fix: `Use transactional writes -- persist new refresh token before making any API calls with the access token`. [src5, src7]
- **Using personal Workday accounts instead of ISU**: If a personal account is used for OAuth API client registration and that employee leaves, all integrations break. Fix: `Always create a dedicated ISU (Integration System User) for each integration; never tie OAuth to human accounts`. [src6]

## Diagnostic Commands

```bash
# --- Salesforce: Test JWT Bearer token acquisition ---
# Requires: openssl, jwt CLI, or Python
python3 -c "
import jwt, time, requests
claim = {'iss':'${CONSUMER_KEY}','sub':'${USERNAME}','aud':'https://login.salesforce.com','exp':int(time.time())+300}
assertion = jwt.encode(claim, open('server.key').read(), algorithm='RS256')
r = requests.post('https://login.salesforce.com/services/oauth2/token', data={'grant_type':'urn:ietf:params:oauth:grant-type:jwt-bearer','assertion':assertion})
print(r.status_code, r.json().get('access_token','ERROR')[:20]+'...')
"

# --- SAP: Test XSUAA client credentials ---
curl -s -o /dev/null -w "%{http_code}" \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d "grant_type=client_credentials" \
  "${XSUAA_TOKEN_URL}"
# Expected: 200

# --- Oracle: Test OCI IAM token ---
curl -s -o /dev/null -w "%{http_code}" \
  -u "${CLIENT_ID}:${CLIENT_SECRET}" \
  -d "grant_type=client_credentials&scope=urn:opc:idm:__myscopes__" \
  "${OCI_IAM_TOKEN_URL}"
# Expected: 200

# --- Dynamics 365: Test Entra ID client credentials ---
curl -s -o /dev/null -w "%{http_code}" \
  -d "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}&scope=${D365_URL}/.default" \
  "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token"
# Expected: 200

# --- NetSuite: Test OAuth 2.0 refresh ---
curl -s -o /dev/null -w "%{http_code}" \
  -d "grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
  "https://${ACCOUNT_ID}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token"
# Expected: 200

# --- Workday: Test refresh token ---
curl -s -o /dev/null -w "%{http_code}" \
  -d "grant_type=refresh_token&refresh_token=${REFRESH_TOKEN}&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
  "https://wd2-impl-services1.workday.com/ccx/oauth2/${TENANT}/token"
# Expected: 200
```

## Version History & Compatibility

| ERP | OAuth Version Milestone | Date | Status | Notes |
|---|---|---|---|---|
| Salesforce | JWT Bearer flow GA | 2016 | Current | No breaking changes since introduction [src1] |
| Salesforce | Device flow GA | 2020 | Current | For CLI/IoT devices |
| SAP S/4HANA Cloud | XSUAA OAuth mandatory (Public Cloud) | 2020 | Current | Communication arrangements required [src2] |
| SAP | X.509 mTLS for XSUAA | 2022 | Current | Alternative to client secret [src2] |
| Oracle Fusion | IDCS OAuth GA | 2018 | Migrating to OCI IAM | IDCS merged into OCI IAM 2023 [src3] |
| Oracle Fusion | Basic auth deprecated for new integrations | 2023 | Current | Existing basic auth still works [src3] |
| NetSuite | OAuth 2.0 GA | 2021 | Current | REST APIs only [src5] |
| NetSuite | TBA (OAuth 1.0) deprecated for new integrations | 2027.1 | Planned | Existing TBA continues [src7] |
| Dynamics 365 | Entra ID (Azure AD renamed) | 2023 | Current | No protocol changes, rebrand only [src4] |
| Dynamics 365 | Federated identity credentials | 2024 | Current | For cross-cloud (GCP, AWS) workloads [src4] |
| Workday | OAuth 2.0 API clients GA | 2020 | Current | Requires ISU linkage [src6] |

### Deprecation Policy

- **Salesforce**: OAuth API versions are not independently deprecated; connected app OAuth flows have been stable since 2016. Salesforce deprecated username-password flow for production use. [src1]
- **SAP**: Basic authentication deprecated for cloud APIs; 12-month notice period for breaking changes via SAP Road Map Explorer. [src2]
- **Oracle**: IDCS-to-OCI IAM migration is automatic; existing IDCS integrations get 12-month migration window. [src3]
- **NetSuite**: TBA (OAuth 1.0) end-of-new-integration at 2027.1; existing integrations continue beyond that date. [src5, src7]
- **Dynamics 365**: Client secrets have configurable expiry (max 24 months). Microsoft recommends certificate-based auth for longevity. [src4]
- **Workday**: No announced deprecation of ISU+OAuth pattern; ISU remains the standard integration identity model. [src6]

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Comparing OAuth 2.0 flows across multiple ERPs for a cross-system integration | Need detailed single-ERP OAuth walkthrough with full troubleshooting | System-specific auth card (e.g., salesforce-oauth-authentication, dynamics-365-authentication) |
| Deciding which OAuth grant type for server-to-server ERP integration | Need non-OAuth auth comparison (SAML, certs, basic auth) | erp-authentication-comparison/2026 |
| Building a universal integration adapter that handles multiple ERP auth flows | Need rate limits, data model, or API capabilities (not auth) | erp-rate-limits-comparison or system-specific API capability cards |
| Planning certificate and secret rotation strategy across ERPs | Need SSO/SAML for end-user login to ERP web interfaces | Not covered -- see vendor SSO documentation |

## Cross-System Comparison

| Capability | Salesforce | SAP S/4HANA | Oracle Fusion | NetSuite | Dynamics 365 | Workday | Notes |
|---|---|---|---|---|---|---|---|
| OAuth 2.0 Provider | Built-in | SAP BTP/XSUAA | OCI IAM | Built-in | Entra ID | Built-in | SF, NS, WD self-contained; others use external IdP |
| Server-to-Server Grant | JWT Bearer | Client Credentials | Client Credentials | Auth Code + Refresh | Client Credentials | Refresh Token | No universal grant type works for all 6 |
| Token Lifetime | ~2h (session) | 12 min | 1h | 60 min | 60-75 min | Minutes | SAP's 12-min TTL is the outlier |
| Refresh Token | Web server only | N/A | 7d default | 7d rolling | N/A | Non-expiring opt. | Rolling (NS/WD) vs static (Oracle) |
| Certificate Auth | X.509 on app | X.509 mTLS | X.509 assertion | No | Certificate on app | No | SAP requires mTLS endpoint |
| Setup Complexity | Medium (connected app + cert) | High (BTP + comm arrangement) | Medium (OCI IAM confidential app) | Low (integration record) | Low (Entra app reg) | Medium (API client + ISU) | SAP highest due to BTP dependency |
| SDK Support | jsforce, simple-salesforce | SAP Cloud SDK | OCI SDK | Standard HTTP | MSAL | Standard HTTP | D365 has best SDK (MSAL) |
| Admin Consent | Pre-authorize via profiles | Implicit via comm arrangement | OCI IAM admin grant | Admin consent in integration | Entra admin consent | ISU security policies | Each ERP's admin model is different |
| Multi-tenant | Per-org connected app | Per-subaccount | Per-IAM-domain | Per-account-id | Per-Entra-tenant | Per-tenant | URL patterns all differ |
| PKCE Support | Yes | No | No | Yes | Yes | No | Required for public clients |

## Important Caveats

- OAuth 2.0 token lifetimes and rate limits are subject to change with each ERP release. The values in this card reflect configurations as of March 2026. Always verify against current vendor documentation.
- SAP S/4HANA on-premise has a completely different OAuth implementation (via SAP Gateway) compared to S/4HANA Cloud (via BTP/XSUAA). This card covers cloud only.
- Oracle's IDCS-to-OCI IAM migration is ongoing. Some tenants may still use IDCS token endpoints while others have been migrated to OCI IAM domain endpoints. Check your specific tenant.
- NetSuite OAuth 2.0 availability depends on the SuiteCloud feature being enabled by an admin. It is not enabled by default on all accounts.
- Dynamics 365 token endpoints use the Microsoft Entra ID platform (formerly Azure AD). The rebranding changed no protocol behavior, but old Azure AD documentation URLs still redirect to Entra ID docs.
- Workday token endpoint URLs vary by data center (e.g., wd2, wd3, wd5) and implementation type (impl vs prod). Always use the exact URL from your Workday tenant configuration.

## Related Units

- [ERP Authentication Comparison: OAuth, Certificates & Service Accounts](/business/erp-integration/erp-authentication-comparison/2026)
- [Salesforce OAuth Authentication Deep-Dive](/business/erp-integration/salesforce-oauth-authentication/2026)
- [Dynamics 365 Authentication](/business/erp-integration/dynamics-365-authentication/2026)
- [Workday Authentication: ISU & OAuth](/business/erp-integration/workday-authentication-isu-oauth/2026)
- [SAP Authentication Methods](/business/erp-integration/sap-authentication-methods/2026)
- [Oracle ERP Cloud Authentication](/business/erp-integration/oracle-erp-cloud-authentication/2026)
- [NetSuite TBA vs OAuth 2.0](/business/erp-integration/netsuite-tba-vs-oauth2/2026)
