Salesforce Dictionary - Free Salesforce GlossarySalesforce Dictionary
All errors
Integration

INVALID_SESSION_ID: Session expired or invalid

Your API call presented a session token Salesforce no longer accepts — it timed out, was logged out from elsewhere, was issued by a different org, or your IP fell outside the user's login restrictions. The fix depends on which of those it is.

Also seen asINVALID_SESSION_ID·Session expired or invalid·INVALID_SESSION_ID: Session expired

An integration that pulls accounts from Salesforce into a billing system has been running for a year. This morning it starts returning INVALID_SESSION_ID: Session expired or invalid on every request. The credentials haven't changed. The Salesforce side is up. Something invalidated the token the integration was using.

What the error means

A Salesforce session id is a token that authenticates an API call. The token has finite lifetime and can be invalidated by several events. When Salesforce receives a call with a token it no longer accepts, it returns INVALID_SESSION_ID. The integration is unauthorized; it needs a fresh token before it can call again.

The fault is upstream of the API call: the token state, not the call itself. Diagnosing requires figuring out why the token is no longer valid.

Five reasons the session became invalid

Token timed out. Sessions have a configurable lifetime (Setup → Session Settings → Timeout Value). The default is 2 hours of inactivity. A token that hasn't been used for the timeout interval is invalidated. The integration must obtain a fresh token before its next call.

User logged out elsewhere. The user whose token the integration holds logged out from a browser or another integration. Logout invalidates all sessions for that user. The integration's token becomes invalid even though it was never used by the logout action.

Password changed. Changing a user's password invalidates all sessions tied to that user. New tokens must be obtained with the new password.

IP address moved. Some orgs configure IP-based login restrictions. If the integration's IP changes (a server move, a CDN routing change, an office relocation), the session might be invalidated for the new IP even though it was valid for the old one.

Org configuration change. Admins can revoke OAuth tokens (Setup → Connected Apps → revoke), force logout (Setup → All Users → freeze/deactivate), or change the session-policy settings. Any of these can invalidate active tokens.

The broken example

An integration that caches a session id forever:

class SalesforceClient:
    def __init__(self):
        self.session_id = None

    def login(self):
        # SOAP login that returns sessionId
        self.session_id = self._authenticate()

    def get_accounts(self):
        return self._call_api('Account', headers={'Authorization': f'Bearer {self.session_id}'})

The client logs in once and reuses the session id for every call. The first time the token times out (2 hours later, by default), every subsequent call fails.

The fix: handle session expiration

The integration must detect INVALID_SESSION_ID responses and re-authenticate automatically. The standard pattern is to catch the error, re-login, and retry the call:

class SalesforceClient:
    def __init__(self):
        self.session_id = None

    def login(self):
        self.session_id = self._authenticate()

    def _call_api(self, endpoint, headers=None):
        headers = headers or {}
        headers['Authorization'] = f'Bearer {self.session_id}'
        response = self._http.get(endpoint, headers=headers)
        if response.status_code == 401 or 'INVALID_SESSION_ID' in response.text:
            self.login()  # Refresh
            headers['Authorization'] = f'Bearer {self.session_id}'
            response = self._http.get(endpoint, headers=headers)
        return response

    def get_accounts(self):
        return self._call_api('/services/data/v60.0/sobjects/Account')

The retry-once pattern handles the timeout case. The integration self-heals from INVALID_SESSION_ID without operator intervention.

The OAuth refresh token alternative

For Connected Apps using OAuth, the cleaner pattern is the refresh-token flow. On initial login, the integration receives both an access token (short-lived) and a refresh token (long-lived). When the access token expires, the integration uses the refresh token to obtain a new access token without re-authenticating from scratch.

class SalesforceOAuthClient:
    def authenticate(self):
        # Initial OAuth flow returns access_token + refresh_token
        result = self._oauth_login()
        self.access_token = result['access_token']
        self.refresh_token = result['refresh_token']
        self.expires_at = time.time() + result['expires_in']

    def get_valid_token(self):
        if time.time() > self.expires_at - 60:  # Refresh 60s before expiry
            self._refresh()
        return self.access_token

    def _refresh(self):
        result = self._oauth_refresh(self.refresh_token)
        self.access_token = result['access_token']
        self.expires_at = time.time() + result['expires_in']

The refresh token survives access-token timeouts. The integration only needs to fully re-authenticate if the refresh token itself is revoked (which is rarer than access-token expiry).

The fixed example, end to end

A Python-style Salesforce client with OAuth refresh and exponential-backoff retries:

import time
import requests

class SalesforceClient:
    def __init__(self, instance_url, client_id, client_secret, refresh_token):
        self.instance_url = instance_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.refresh_token = refresh_token
        self.access_token = None
        self.expires_at = 0

    def _refresh(self):
        url = f'{self.instance_url}/services/oauth2/token'
        data = {
            'grant_type': 'refresh_token',
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'refresh_token': self.refresh_token,
        }
        resp = requests.post(url, data=data)
        resp.raise_for_status()
        payload = resp.json()
        self.access_token = payload['access_token']
        self.expires_at = time.time() + payload.get('expires_in', 7200) - 60

    def _ensure_token(self):
        if time.time() > self.expires_at:
            self._refresh()

    def get(self, path, retries=3):
        self._ensure_token()
        for attempt in range(retries):
            headers = {'Authorization': f'Bearer {self.access_token}'}
            resp = requests.get(f'{self.instance_url}{path}', headers=headers)
            if resp.status_code == 401:
                self._refresh()
                continue
            return resp
        raise RuntimeError(f'Failed after {retries} retries')

The client proactively refreshes the token before expiry, retries on 401 (which Salesforce returns for INVALID_SESSION_ID), and gives up after a configurable number of attempts.

The Connected App and OAuth scopes

Modern integrations should use Connected Apps with OAuth, not the legacy SOAP login API. A Connected App is configured in Setup → App Manager. The configuration includes:

  • OAuth callback URL.
  • Selected OAuth scopes (which APIs the app can call).
  • Refresh-token policy (rotate, do not rotate, immediately expire).

The "Refresh token is valid until revoked" policy is the most common; the token survives until an admin revokes it. The "Immediately expire refresh token" policy expires the refresh token on every use, which forces the integration to re-authenticate often (rare in modern setups).

When the IP address moves

For orgs with IP-based login restrictions, a server move (cloud provider relocates your VM, a CDN re-routes through a new edge) can invalidate sessions because the new source IP isn't in the login range.

The fix: either widen the IP range to cover all possible source IPs, or move the integration to a server with a stable IP. Some teams route through a dedicated proxy with a static IP to handle this.

Session timeout vs OAuth token expiry

Two timeouts apply:

  • Session timeout (Setup → Session Settings): UI session timeout. Applies to API calls that use a session id from the UI login.
  • OAuth token expiry (Connected App settings): how long an OAuth access token lasts before expiring.

These are different. An OAuth access token has its own expiry independent of the user's UI session. The refresh token has yet another expiry policy.

For OAuth integrations, focus on the access-token expiry. For SOAP/REST API integrations that use a SOAP login, the session timeout applies.

Diagnosing in production

When INVALID_SESSION_ID fires in production:

  1. Check the timestamp of the last successful call. Was the gap longer than the session timeout?
  2. Check whether any admin made changes to the integration user (password change, deactivation, freeze).
  3. Check whether any admin revoked the Connected App's tokens.
  4. Check whether the source IP changed (compare current IP to the IP at last successful call).

The four checks cover 99% of cases. Each takes a minute or two.

Test patterns

A unit test for the session-refresh logic:

def test_client_refreshes_on_401():
    client = SalesforceClient('https://acme.my.salesforce.com', 'cid', 'csec', 'rt')
    client.access_token = 'old_token'
    client.expires_at = time.time() + 3600

    mock = MagicMock()
    mock.get.side_effect = [
        MockResponse(401, '{"errorCode": "INVALID_SESSION_ID"}'),
        MockResponse(200, '{"records": []}')
    ]
    # Inject mock and assert refresh happens

The test confirms that a 401 response triggers a refresh and a retry. Regression tests should cover both the proactive-refresh path (token expires before next call) and the reactive-refresh path (401 after call).

A subtle case: clock skew

If the integration's local clock is significantly off from Salesforce's clock, proactive refresh logic that checks expires_at can be wrong. The integration thinks the token is still valid; Salesforce has already invalidated it. The 401 fires on the next call.

The mitigation: subtract a buffer (60-300 seconds) from the local time when comparing to expires_at. The buffer absorbs reasonable clock-skew amounts.

Logging and monitoring

For long-running integrations, log every authentication event (login, refresh, 401-triggered re-login). The logs reveal patterns: an integration that re-logins many times per hour has a different problem than one that logins once per day. Patterns inform fixes.

A Lightning dashboard on the Salesforce side, querying Login History, shows integration login activity. Correlate spikes with deploy events or incident timestamps.

Avoiding the legacy SOAP login

Salesforce supports a SOAP-based login API that returns a session id directly. New integrations should not use it. The OAuth flows are more secure, more flexible, and better instrumented.

If you're maintaining an old integration that uses SOAP login, the path forward is to migrate to OAuth. The migration involves:

  1. Creating a Connected App in Salesforce.
  2. Configuring the OAuth callback URL or using the client-credentials flow.
  3. Updating the integration code to obtain access tokens via OAuth.
  4. Implementing token refresh.

The migration is non-trivial but pays off in fewer incidents and better security posture.

Named Credentials for Apex callouts

For Salesforce-internal integrations (Apex code calling external systems), Named Credentials handle authentication centrally. The integration calls callout:NamedCred/path and Salesforce manages the OAuth token, refresh, and IP-allowlist concerns transparently.

For external systems calling into Salesforce (the case we're discussing), Named Credentials don't apply; the external system manages its own credentials.

A reusable token-management library

For organizations with many Salesforce integrations, building a shared token-management library is worthwhile. The library:

  • Wraps the OAuth refresh flow.
  • Caches tokens in a shared store (Redis, encrypted file, env-specific).
  • Handles 401 retries automatically.
  • Logs authentication events.

Each integration uses the library instead of rolling its own auth. The library evolves over time with new OAuth flows, new error patterns, new monitoring needs. Every integration inherits the improvements.

Specific to Apex callouts back into Salesforce

If your Apex code makes a self-callout (Salesforce calls itself via REST), the session id used is the running user's session. The session expires with the user's UI session. Self-callouts are rare and usually a sign that the architecture should be reconsidered; direct Apex usually does the work without a round-trip.

If you must self-callout, use a dedicated integration user with a long-lived session (or Named Credentials configured for self-callouts) rather than the running user's transient session.

Anti-pattern: storing session id in source code

Some legacy integrations hard-code a session id in source. This is fragile (the id is short-lived) and insecure (the id grants access). Move session ids to a secrets management system (Vault, AWS Secrets Manager, Salesforce Custom Settings/Metadata with encrypted fields) and refresh from there.

Quick recovery when the integration is down

For a production incident where INVALID_SESSION_ID is firing on every call:

  1. Re-authenticate manually using your OAuth flow.
  2. Verify the new token works against a known-good endpoint.
  3. Update the integration's stored credentials with the new token.
  4. Restart the integration.

Most incidents resolve in 5-10 minutes once you've identified the cause.

Further reading from Salesforce

Related dictionary terms

Share this fix

Share on LinkedInShare on X

Related Integration errors