OAuth Field Guide
Researched guide · 2026-10-28

Salesforce

www.salesforce.com · CRM · for: AI agent reads and updates CRM records (accounts, opportunities) in each customer's Salesforce org.

InvolvedTenant-scopedwhy this rating →
Do this next

Sign up for a free Developer Edition org and decide now whether you'll join the Salesforce Partner Program (Partner Business Org as Dev Hub). That decision determines where your packaged External Client App, and therefore your client ID, will live.

Your path at a glance

Prepare
Register
Configure OAuth
Test
Review
Launch
Customer
Agent
You
Provider
Customer admin
Milestones
◆ Works for you
30 min – 1.5 h work
no wait
◆ Any customer
10 h – 40 h work
no wait
agent can do you agent prepares, you finish assumedClick a step to jump to it.

Watch out for

  • BlockerOld tutorials say 'create a connected app' — you can't anymorewhy & fix →
  • BlockerA local ECA only serves its own orgwhy & fix →
  • WarningUninstalled apps are blocked for new users (since Sept 2025)why & fix →
  • WarningRefresh tokens die after 30 idle days (Nov 4, 2026 in production)why & fix →
  • WarningRotation races and silent revocationswhy & fix →
  • WarningProfessional/Group Edition customers can't use the APIwhy & fix →
  • WarningA Developer Edition Dev Hub can strand your packagewhy & fix →
  • InfoAPI calls to login.salesforce.com failwhy & fix →

Before you start

Steps

Prepare

  1. 1Sign up for a Developer Edition orgYouopen ↗details →
  2. 2Choose your packaging home: Dev Hub and namespaceYouopen ↗details →

Configure OAuth

  1. 4Implement the web-server flow with PKCE and per-org storageAgent can doopen ↗details →
  2. 5Build and promote a 2GP managed package containing the ECAAgent can doopen ↗details →

Test with your account

  1. 6Connect your own Developer Edition orgAgent prepares, you finishopen ↗details →
  2. 7Install into a second org and prove cross-org OAuthAgent prepares, you finishdetails →

Review & verification

  1. 8(Optional) AgentExchange listing and security reviewProvider reviewsopen ↗details →

Launch to customers

  1. 9Publish an admin install guideAgent can dodetails →

Customer-side setup

  1. 10Customer admin installs the package and sets policiesCustomer admindetails →
  2. 11Customer users connect, and you keep tokens aliveCustomer admindetails →

Copy & paste

Scopes to request
apirefresh_tokenid
Developer console
https://login.salesforce.com/
OAuth docs
https://help.salesforce.com/s/articleView?id=xcloud.external_client_apps.htm&type=5
Authorize URL
https://login.salesforce.com/services/oauth2/authorize
Token URL
https://login.salesforce.com/services/oauth2/token
Discovery
https://login.salesforce.com/.well-known/openid-configuration
Prompt for your coding agent
Show prompt
You are adding a multi-tenant Salesforce integration to this codebase so an AI agent can read and update Accounts and Opportunities in each customer's Salesforce org.

Constraints (as of Oct 2026):
- Use an External Client App (ECA), not a connected app. New connected apps can't be created.
- OAuth 2.0 web-server flow with PKCE (S256), sending both client_secret and code_verifier. Scopes: 'api refresh_token id'.
- Login host depends on the connection: https://login.salesforce.com (production and Developer Edition), https://test.salesforce.com (sandbox), or a customer-supplied https://<mydomain>.my.salesforce.com. Validate that My Domain input ends in .my.salesforce.com.
- Persist per connection: org ID and user ID (parsed from the 'id' URL), instance_url, access_token, refresh_token, and issued_at. Encrypt tokens at rest.
- ALL REST calls go to {instance_url}/services/data/v68.0/... Never call login.salesforce.com, which returns URL_NOT_RESET.
- Refresh token rotation is on: after each refresh, atomically store the NEW refresh_token. Serialize refreshes per connection with a lock.
- Add a scheduled keep-alive that refreshes any connection idle 20+ days, because Salesforce expires refresh tokens idle for 30 days. Treat invalid_grant as 'needs reconnect' and notify the user.
- Map OAUTH_APPROVAL_ERROR_GENERIC and OAUTH_AUTHORIZATION_BLOCKED callback errors to a UI message telling the user their Salesforce admin must install our package (include INSTALL_URL from env).
- Handle API_DISABLED_FOR_ORG (edition lacks API access) and REQUEST_LIMIT_EXCEEDED (org's 24 h API allocation is shared with the customer's other integrations), and back off.

Deliverables:
1. /api/salesforce/connect route that builds the authorize URL (state + PKCE stored server-side) and /api/salesforce/callback route.
2. A SalesforceClient with query(soql), getRecord, updateRecord (PATCH /sobjects/{type}/{id}), and automatic refresh on 401.
3. Keep-alive job and tests (mock HTTP).
4. A metadata folder 'force-app/main/default' containing the ECA header (.eca) and OAuth settings (.ecaOauth) from the artifacts, plus sfdx-project.json for a 2GP managed package. Do NOT create or commit global OAuth settings or secrets.
Read config from the .env template. Don't hardcode secrets.
.env template
Show bash
# Salesforce External Client App (consumer key/secret come from Setup > External Client App Manager > your app > OAuth Settings)
SALESFORCE_CLIENT_ID=your-eca-consumer-key
SALESFORCE_CLIENT_SECRET=your-eca-consumer-secret
SALESFORCE_REDIRECT_URI=https://app.example.com/api/salesforce/callback
SALESFORCE_SCOPES="api refresh_token id"
SALESFORCE_API_VERSION=v68.0
# Default login hosts; per-connection override for My Domain
SALESFORCE_LOGIN_URL=https://login.salesforce.com
SALESFORCE_SANDBOX_LOGIN_URL=https://test.salesforce.com
# Managed package install link shown to customer admins
SALESFORCE_PACKAGE_INSTALL_URL=https://login.salesforce.com/packaging/installPackage.apexp?p0=04tXXXXXXXXXXXXXXX
TOKEN_ENCRYPTION_KEY=generate-a-32-byte-random-key
force-app/main/default/externalClientApps/AiAgentConnector.eca-meta.xml
Show xml
<?xml version="1.0" encoding="UTF-8"?>
<ExternalClientApplication xmlns="http://soap.sforce.com/2006/04/metadata">
    <contactEmail>integrations@example.com</contactEmail>
    <description>Lets the Example AI agent read and update Accounts and Opportunities.</description>
    <distributionState>Packaged</distributionState>
    <isProtected>false</isProtected>
    <label>AI Agent Connector</label>
</ExternalClientApplication>
force-app/main/default/extlClntAppOauthSettings/AiAgentConnector_oauth.ecaOauth-meta.xml
Show xml
<?xml version="1.0" encoding="UTF-8"?>
<ExtlClntAppOauthSettings xmlns="http://soap.sforce.com/2006/04/metadata">
    <commaSeparatedOauthScopes>Api, RefreshToken, OpenID</commaSeparatedOauthScopes>
    <externalClientApplication>AiAgentConnector</externalClientApplication>
    <label>AiAgentConnector_oauth</label>
</ExtlClntAppOauthSettings>
<!-- Callback URL, PKCE (isPkceRequired) and consumer credentials live in ExtlClntAppGlobalOauthSettings, which can't be packaged or committed; set them in Setup on the Dev Hub/PBO org. -->
Token exchange (TypeScript sketch)
Show typescript
export async function exchangeCode(loginUrl: string, code: string, codeVerifier: string) {
  const res = await fetch(`${loginUrl}/services/oauth2/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      code_verifier: codeVerifier,
      client_id: process.env.SALESFORCE_CLIENT_ID!,
      client_secret: process.env.SALESFORCE_CLIENT_SECRET!,
      redirect_uri: process.env.SALESFORCE_REDIRECT_URI!,
    }),
  });
  const body = await res.json();
  if (!res.ok) throw new Error(`${body.error}: ${body.error_description}`);
  // body.instance_url is where ALL data API calls must go; body.id ends in /{orgId}/{userId}
  const [orgId, userId] = new URL(body.id).pathname.split("/").slice(-2);
  return { orgId, userId, instanceUrl: body.instance_url, accessToken: body.access_token, refreshToken: body.refresh_token };
}

Why & details

The reasoning, evidence and sources behind everything above. Hover a badge to see its sources.

The full picture

Tenant-scoped

OAuth works against your own free Developer Edition org within about an hour, but serving other companies' orgs now means a packaged External Client App inside a second-generation managed package that each customer's admin installs. New connected apps have been blocked since Spring '26, uninstalled apps have been blocked for new users since September 2025, and refresh tokens idle for 30 days expire starting November 4, 2026 (production), so plan your packaging, the admin install step, and token keep-alive before you write any API code.

Each customer is a separate Salesforce org with its own instance URL (My Domain), admin-controlled access policies (permitted users, IP relaxation, refresh token policy) and edition-dependent API access. Since September 2025 an app must be installed in the customer's org before new users can authorize it. External Client Apps (the required replacement for connected apps) must be packaged and installed per org to work across orgs. You don't need Salesforce's approval to reach customers. The gate is each customer's admin, which is the tenant-scoped pattern rather than partner-gated.

Rating: Expect reviews, admin approvals or per-customer setup measured in days.

Working with your own account
30 min – 1.5 h hands-on · no wait

Free Developer Edition org, plus a local External Client App created in that same org with OAuth enabled, a callback URL, the api and refresh_token scopes, and PKCE. Works only for users of that one org.

Any customer can connect
10 h – 40 h hands-on · no wait

A packaged ECA in a promoted second-generation managed package (registered namespace, Dev Hub, ideally a Partner Business Org) that each customer admin installs and configures. No Salesforce review is needed unless you list on AgentExchange or need Group/Professional Edition access, which adds a security review of unknown length.

What affects your setup

Before you start — why each matters

How well this guide is backed

12 tested30 source-backed2 community-reported1 uncertain· Well evidenced (96% backed)

Step details

  1. 1Sign up for a Developer Edition org↑ back to step

    Create a free Developer Edition org to build and test in. Don't build in a customer's production org.

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Draft the signup details and a checklist. A person has to submit the form and verify the email.

    https://developer.salesforce.com/signup ↗
  2. 2Choose your packaging home: Dev Hub and namespace↑ back to step

    Decide where your packaged ECA will live. The recommended route is a Partner Business Org, through the Salesforce Partner Program, used as Dev Hub, with 2GP enabled and a namespace registered in a separate Developer Edition org and linked to it. A Developer Edition Dev Hub works for experiments, but packages tied to it can't be updated once it expires.

    👤 Why you: A product/business judgement only you can make

    🤖 What an agent can do: Generate sfdx-project.json with the namespace and run 'sf org login web --set-default-dev-hub' once a person has logged in.

    https://partners.salesforce.com/ ↗
  3. In the Dev Hub/PBO org, go to Setup > External Client App Manager > New. Set Distribution State to Packaged, enable OAuth, add your HTTPS callback, and select the scopes Api, RefreshToken and optionally OpenID. Turn on 'Require PKCE'. Also turn on refresh token rotation, the 30-day idle refresh TTL and the refresh-token IP allowlist, which Salesforce requires of partner apps used by more than two customer production orgs (deadline May 11, 2026). Then copy the consumer key and secret. They live in global OAuth settings, which are never packaged.

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Author the ExternalClientApplication (.eca) and ExtlClntAppOauthSettings (.ecaOauth) metadata and deploy them with 'sf project deploy start'. The person copies the consumer key and secret from Setup into your secret store.

    https://login.salesforce.com/ ↗
  4. Build the authorize URL on login.salesforce.com, test.salesforce.com or the customer's My Domain, with code_challenge_method=S256. Exchange the code with client_secret and code_verifier. Persist access_token, refresh_token, instance_url and the id URL (org ID) per connection. On every refresh, store the new rotated refresh token atomically.

    🤖 What an agent can do: Write the OAuth routes, an encrypted token store keyed by org ID and user ID, a refresh mutex to avoid rotation races, and a client that always uses instance_url plus /services/data/v68.0.

    https://login.salesforce.com/.well-known/openid-configuration ↗
  5. 5Build and promote a 2GP managed package containing the ECA↑ back to step

    Create the package and a version with code coverage (promotion needs 75% if the package contains Apex), then promote it to released so production orgs can install it. Promotion can't be undone.

    🤖 What an agent can do: Run 'sf package create --package-type Managed', then 'sf package version create --code-coverage --installation-key-bypass', then 'sf package version promote'. Capture the 04t install ID and build the install URL.

    https://resources.docs.salesforce.com/latest/latest/en-us/sfdc/pdf/pkg2_dev.pdf ↗
  6. 6Connect your own Developer Edition org↑ back to step

    Complete the flow as your own user and run a SOQL query against Account and a PATCH against an Opportunity. This proves the code before you take on packaging.

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Write an integration test script for SELECT Id, Name FROM Account LIMIT 5 and PATCH /sobjects/Opportunity/{id}. A person clicks through the consent screen once.

    https://login.salesforce.com/services/oauth2/authorize ↗
  7. 7Install into a second org and prove cross-org OAuth↑ back to step

    Install the package into a separate Developer Edition org and connect a user from that org. If you see OAUTH_AUTHORIZATION_BLOCKED 'Cross-org OAuth flows are not supported for this external client app', the installed ECA isn't linked to your global OAuth settings. Fix this before any customer touches it.

    👤 Why you: Needs your logged-in console session (2FA/CAPTCHA)

    🤖 What an agent can do: Script the install with 'sf package install' and re-run the integration test against the second org. Capture the error payload if it fails.

  8. 8(Optional) AgentExchange listing and security review↑ back to step

    Do this only if you want a public listing, Group/Professional Edition REST access, or partner benefits. It requires Partner Program membership and a security review submission (test environments plus documentation). The timeline isn't published.

    👤 Why you: Waiting on the provider's reviewers

    🤖 What an agent can do: Run Salesforce Code Analyzer (sf code-analyzer run --rule-selector AppExchange --rule-selector Recommended:Security) and draft the security review documentation.

    https://partners.salesforce.com/ ↗
  9. 9Publish an admin install guide↑ back to step

    Give customers the package install URL and step-by-step instructions: which profiles or permission sets to grant, the recommended policies, and how to choose production vs sandbox login.

    🤖 What an agent can do: Generate the admin guide and an in-app 'Connect Salesforce' screen with a production/sandbox/My Domain selector.

  10. The admin installs the package and opens the External Client App's policies. They choose Permitted Users (AdminApprovedPreAuthorized plus a permission set is the common enterprise choice), IP relaxation, and the refresh token policy. They also make sure connecting users have API Enabled and Account/Opportunity edit rights.

    👤 Why you: Your customer's admin must act

    🤖 What an agent can do: Pre-fill a permission set definition in the package, for example 'AI Agent Connector User', so the admin only has to assign it.

  11. Users click Connect and approve the scopes. Your backend refreshes each connection at least every few weeks so it never sits idle for 30 days, and treats invalid_grant as 'reconnect needed'.

    👤 Why you: Your customer's admin must act

    🤖 What an agent can do: Add a scheduled job that refreshes tokens idle for 20 days or more, plus alerting and a reconnect email when refresh fails.

Gotchas explained

Why each scope

ScopeWhy you need itTierEvidence
apiQuery and update Accounts, Opportunities and other records through the REST API, acting as the connecting user. Access is bounded by that user's profile, permission sets and sharing rules.sensitive
refresh_tokenGet a refresh token so the agent can act while the user is away. offline_access is a synonym.basic
idOptional. Gets the identity URL (org ID and user ID) so you can key connections by org. openid works too if you want an ID token.basic

Reference facts

What we verified — and what we couldn't

Live probes (no credentials)

  • ✓discovery https://login.salesforce.com/.well-known/openid-configurationHTTP 200; issuer https://login.salesforce.com; authorize https://login.salesforce.com/services/oauth2/authorize; PKCE methods: S256[P1]
  • ✓authorize https://login.salesforce.com/services/oauth2/authorizeHTTP 400; error "invalid_client_id"; → endpoint is live and rejects the unknown client[P2]
  • ✓token https://login.salesforce.com/services/oauth2/tokenHTTP 400; error "invalid_client_id: client identifier invalid"; → token endpoint is live and rejects a bogus code[P3]
  • ✓page https://login.salesforce.com/HTTP 200; title "Login | Salesforce"[P4]
  • ✓page https://help.salesforce.com/s/articleView?id=xcloud.external_client_apps.htm&type=5HTTP 301; redirects to help.salesforce.com/s/articleView[P5]

Open questions

  • What exact configuration lets a packaged ECA authorize users in subscriber orgs using your single consumer key?
    Official docs say a distributed ECA either references the source org's global OAuth settings or generates new ones in the subscriber org. A 2026 Trailblazer thread shows a packaged ECA still failing cross-org, and nobody posted a confirmed fix. The help.salesforce.com ECA pages didn't render for us.
    To resolve: Install your promoted package into a second Developer Edition org and test before launch. Check the installed ExtlClntAppOauthSettings OAuth link, and open a Partner Support case if it fails.
  • Does the 30-day idle refresh-token limit apply to every app automatically, or only when the 'Limit Idle Refresh Token TTL' option is on?
    The security roadmap (published Oct 7, 2026) lists it as an enforced change with sandbox and production dates. The ISVforce Guide describes it as a checkbox partners turn on. The two docs read differently.
    To resolve: Read the Winter '27 release notes and test in a sandbox after October 21, 2026 by leaving a token idle. Design for the 30-day limit either way.
  • Do the partner security controls (PKCE, rotation, idle TTL, IP allowlist; due May 11, 2026) bind a vendor that isn't in the Salesforce Partner Program?
    The ISVforce Guide applies them to any connected app or ECA 'used in connection with a Partner Application' in more than two customer production orgs, with suspension as the penalty. Whether a non-partner counts as a 'Partner' isn't spelled out.
    To resolve: Enable all four controls anyway, since they're good practice and cheap. Confirm scope with Salesforce partner support if you won't join the program.
  • Could the advertised dynamic client registration endpoint (/services/oauth2/register) replace per-org packaging?
    Discovery advertises registration_endpoint, and also mcp_api and a2a_api scopes. We couldn't find docs saying third-party apps may self-register in arbitrary customer orgs. It's likely scoped to Salesforce-hosted MCP clients.
    To resolve: Find the Salesforce hosted MCP server docs on dynamic registration and test against a Developer Edition org. Don't rely on it for production until it's documented.

Ask the provider

A ready-to-send message built from the open questions above.

Send to
No contact channel found — check https://help.salesforce.com/s/articleView?id=xcloud.external_client_apps.htm&type=5 for developer support
Subject
OAuth integration questions — Salesforce
Hi Salesforce developer support team,

We're building an integration where AI agent reads and updates CRM records (accounts, opportunities) in each customer's Salesforce org. Before we register an OAuth app and launch to customers, we couldn't find answers to the following in your documentation:

1. What exact configuration lets a packaged ECA authorize users in subscriber orgs using your single consumer key?
   (What we found: Official docs say a distributed ECA either references the source org's global OAuth settings or generates new ones in the subscriber org. A 2026 Trailblazer thread shows a packaged ECA still failing cross-org, and nobody posted a confirmed fix. The help.salesforce.com ECA pages didn't render for us.)
2. Does the 30-day idle refresh-token limit apply to every app automatically, or only when the 'Limit Idle Refresh Token TTL' option is on?
   (What we found: The security roadmap (published Oct 7, 2026) lists it as an enforced change with sandbox and production dates. The ISVforce Guide describes it as a checkbox partners turn on. The two docs read differently.)
3. Do the partner security controls (PKCE, rotation, idle TTL, IP allowlist; due May 11, 2026) bind a vendor that isn't in the Salesforce Partner Program?
   (What we found: The ISVforce Guide applies them to any connected app or ECA 'used in connection with a Partner Application' in more than two customer production orgs, with suspension as the penalty. Whether a non-partner counts as a 'Partner' isn't spelled out.)
4. Could the advertised dynamic client registration endpoint (/services/oauth2/register) replace per-org packaging?
   (What we found: Discovery advertises registration_endpoint, and also mcp_api and a2a_api scopes. We couldn't find docs saying third-party apps may self-register in arbitrary customer orgs. It's likely scoped to Salesforce-hosted MCP clients.)

Could you point us to the right documentation or process — including whether any review or approval is needed before customers outside our own account can connect, and the typical timeline?

Thanks,
[Your name]
[Company · website]

Sources

  1. S1New connected apps can no longer be created in Spring '26 (Salesforce Help knowledge article 005228017)official
  2. S2Prepare for Salesforce Connected App Usage Restrictions Change (Salesforce Help knowledge article 005132365)official
  3. S3Salesforce security roadmap: OAuth security requirements and connected app end of support (knowledge article 005461287)official
  4. S4REST API Developer Guide, Version 68.0 Winter '27 (PDF)official
  5. S5Salesforce Developer Limits and Allocations Quick Reference (PDF)official
  6. S6ISVforce Guide: Build and Distribute AgentExchange Solutions, Version 68.0 (PDF) — 'Secure Your Connected Apps and External Client Apps', GE/PE API access, security reviewofficial
  7. S7Second-Generation Managed Packaging Developer Guide, Version 68.0 (PDF)official
  8. S8Metadata API Developer Guide (PDF) — ExternalClientApplication, ExtlClntAppOauthSettings, ExtlClntAppGlobalOauthSettings, ExtlClntAppOauthConfigurablePoliciesofficial
  9. S9Mobile SDK Development Guide (PDF) — OAuth 2.0 Refresh Token Flowofficial
  10. S10Trailblazer Community: Packaged External Client App — subscriber OAuth blocked with 'Cross-org OAuth flows are not supported'community
  11. S11Unified.to: Salesforce External Client Apps on multiple organizationscommunity
  12. S12Paragon: Salesforce Connected App changescommunity
  13. S13Nango: Salesforce OAuth refresh token invalid_grant — what it means and how to fix itcommunity
  14. T1Probe: GET https://login.salesforce.com/.well-known/openid-configuration (200; S256 only, refresh_token/offline_access/api/mcp_api scopes, private_key_jwt, DPoP)probe
  15. T2Probe: GET authorize with client_id=probe-invalid → HTTP 400, body error=invalid_client_id&error_description=client identifier invalid (no redirect)probe
  16. T3Probe: POST token endpoint with bogus code/client → HTTP 400 {"error":"invalid_client_id","error_description":"client identifier invalid"}probe
  17. T4Probe: GET https://login.salesforce.com/services/data/ → HTTP 400 URL_NOT_RESETprobe
  18. T5Probe: GET https://test.salesforce.com/.well-known/openid-configuration → 200, issuer https://test.salesforce.comprobe
  19. P1Our probe: discovery check on login.salesforce.comprobe
  20. P2Our probe: authorize check on login.salesforce.comprobe
  21. P3Our probe: token check on login.salesforce.comprobe
  22. P4Our probe: page check on login.salesforce.comprobe
  23. P5Our probe: page check on help.salesforce.comprobe