Blog
Salesforce Is Retiring SOAP login(): Move Your PHP Integration to OAuth Before Summer '27
If your PHP code still signs in to Salesforce with a username, password and security token, it has a deadline. Here's how to find it and what to move it to.
Caleb Dorris 21 October 2026, 9:21 PM Updated 30 October 2026 7 min

A lot of PHP that talks to Salesforce starts the same way: build a SoapClient from a WSDL, call login() with a username and a password with the security token glued on the end, grab the session ID, and go. It worked for years, so nobody touched it.
That's ending. Salesforce is retiring the SOAP API login() call in API versions 31.0 through 64.0 with the Summer '27 release. It's already gone in 65.0 and later. Once your org is upgraded to Summer '27, those calls stop working. I write and maintain Salesforce integrations in PHP, so here's the plain version of what's changing and how I'd move an integration off it.
Rather not dig through this yourself? I do these migrations. Get a free quote, or read on and I'll show you how I'd go about it.
Only the sign-in is going away
This is the part that keeps it from being a rewrite. Salesforce isn't retiring the SOAP API. It's retiring the login() call. Your queries, creates and updates can stay exactly as they are.
Salesforce's own migration article says it directly: an OAuth access token replaces the SOAP session ID, and the underlying SOAP calls don't need to change. You swap how you get the session, not what you do with it.
There's a side benefit. login() doesn't exist in 65.0 and later, so an integration that still uses it is stuck on 64.0 or older. Once it's on OAuth, you can move to a current API version on your own schedule.
The deadline is closer than Summer '27
Summer '27 is the hard stop, but Salesforce has stricter rules landing well before it:
- December 1, 2026: SOAP
login()only succeeds if the user has the Use Any API Auth permission. Without it, the call fails withINSUFFICIENT_ACCESS. - December 15, 2026 (sandboxes) and January 31, 2027 (production):
login()from a broad login IP range requires a security token, or it fails withTOKEN_REQUIRED. - Same dates: SOAP
login()gets turned off in orgs with nologin()activity in the trailing six months.
That last one is the trap. The annual job, the disaster-recovery script, the seasonal import: they look idle, then they fail the one day you need them. Orgs created from Winter '26 onward also have login() disabled by default.
The API versions themselves are moving too. Versions 21.0 through 30.0 were retired in Summer '25. Versions 31.0 through 40.0 are deprecated in Summer '27 and removed in Summer '28. If your WSDL is old enough to be in that range, update it while you're in there.
Find what's still calling login()
You can't migrate what you can't find. Salesforce gives you two places to look.
Login History. In Setup, look for entries with Login Subtype "SOAP API" and API Type "SOAP Enterprise," "SOAP Partner" or "SOAP Tooling." The Application column shows "N/A" for these, which is expected. The Username tells you which integration user to chase down.
The API Total Usage event log. Every org gets the last 24 hours of it, or 30 days with Event Monitoring. Filter for API_FAMILY = SOAP and API_RESOURCE = login. USER_NAME, CLIENT_NAME and CLIENT_IP help you match a call to a server.
Then do the low-tech part: ask whoever owns each server what runs on it. Grep the codebase for ->login( and for .wsdl. The forgotten cron job always turns up.
Pick an OAuth flow and an external client app
For server-to-server work with no person clicking anything, Salesforce points you at two flows:
- Client credentials. The app trades its consumer key and secret for an access token that runs as an integration user you assign. Salesforce calls it the simplest replacement for SOAP
login(). - JWT bearer. The app signs a short-lived JWT with a private key, and Salesforce checks it against a certificate you upload. There's no shared secret in the request, which is why Salesforce recommends it for sensitive integrations.
Neither flow issues a refresh token. You just request a new access token when the old one expires.
Set these up in an external client app, not a connected app. Starting with Spring '26, Salesforce blocks creating new connected apps in both Setup and the Metadata API unless Support turns it back on. Existing connected apps keep working, but new integrations are expected to use external client apps.
Here's the client credentials version in plain PHP with cURL. No library needed:
<?php
// Credentials come from the environment or a secrets store, never the repo.
$myDomain = getenv('SF_MY_DOMAIN'); // e.g. https://yourcompany.my.salesforce.com
$clientId = getenv('SF_CLIENT_ID');
$secret = getenv('SF_CLIENT_SECRET');
$ch = curl_init($myDomain . '/services/oauth2/token');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERPWD => $clientId . ':' . $secret, // Basic auth header
CURLOPT_POSTFIELDS => http_build_query(['grant_type' => 'client_credentials']),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status !== 200) {
throw new RuntimeException('Salesforce token request failed: HTTP ' . $status);
}
$token = json_decode($body, true);
// $token['access_token'] stands in for the old session ID.
// $token['instance_url'] is where your API calls go.
Two details from Salesforce's docs that trip people up. The client credentials flow doesn't work against login.salesforce.com or test.salesforce.com; you have to use your My Domain URL. And you can't request scopes on the token call. Scopes are set on the external client app.
From there, existing SoapClient code keeps working if you point it at instance_url plus your SOAP path and put the access token in the SessionHeader where the old session ID went. REST calls take it as Authorization: Bearer.
For JWT bearer, the shape is the same. You build a header of {"alg":"RS256"} and a claims set with iss (the consumer key), sub (the integration user's username), aud (https://login.salesforce.com, or test for a sandbox) and a short exp. You sign it with openssl_sign() using OPENSSL_ALGO_SHA256, base64url-encode it, and post it as assertion with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Keep the private key outside the web root.
A migration checklist
- Inventory every
login()caller with Login History, API Total Usage and a grep. - Give each integration its own integration user with only the permissions it needs.
- Create an external client app and enable client credentials or JWT bearer.
- Store the secret or private key outside the web root, in the environment or a vault. Rotate it on a schedule.
- Swap
login()for the token request. Keep the data calls as they are. - Cache the access token, and request a new one when a call comes back unauthorized.
- Test in a sandbox, including the jobs that only run once a quarter.
- Turn on Salesforce's release update test run to simulate
login()being disabled before it's real. - Once nothing calls
login(), move off old API versions.
What I'd do this week
Find the callers. The OAuth code is the easy part. It's maybe an afternoon for a clean integration. The hard part is the integration nobody remembers until it breaks on a Monday in December.
Need a hand?
Stuck with SOAP login() on an older PHP stack?
If a cron job, a CRM sync or a billing script on older PHP code still signs in to Salesforce with login(), it stops authenticating once your org is upgraded to Summer '27. It can fail sooner, too: on December 1, 2026 if the user is missing the Use Any API Auth permission, or at the December 15 (sandbox) and January 31 (production) checks. The fix is moving the sign-in to OAuth 2.0, using client credentials or JWT bearer, and caching the access token. Honestly, that's not much code. The slow part is finding the script somebody set up years ago and forgot about, and yes, sometimes that somebody was me.
How I handle it:
- Find every caller. Login History, the API Total Usage log and a grep of your servers, so we know which scripts, endpoints and payloads still depend on
login(). - Swap the sign-in, not the data calls. An external client app with client credentials or JWT bearer, a dedicated integration user, the key kept out of the web root, and a cached token that gets refreshed when it expires.
- Test side by side, then switch. I run the new sign-in in a sandbox next to the old one, including the jobs that only run once a quarter, and use Salesforce's test run that blocks
login()before production gets cut over.
The aim is a switch nobody on your team notices. I won't promise zero downtime, but I'll tell you up front what could go sideways and when the change goes in. If this sounds like your setup, get a free quote.
Caleb Dorris, senior software developer in Nashville
Sources
- Salesforce Help: Platform SOAP API login() Retirement & new Security Requirements
- Salesforce Help: SOAP API login() retirement in versions 31.0–64.0 (Summer '27), how to inventory and migrate integrations to OAuth
- Salesforce Help: Salesforce Platform API Versions 31.0 through 40.0 Retirement
- Salesforce Help: Salesforce Platform API Versions 21.0 through 30.0 Retirement
- Salesforce Help: New Connected Apps Can No Longer Be Created in Spring '26
- Salesforce Help: OAuth 2.0 Client Credentials Flow for Server-to-Server Integration
- Salesforce Help: OAuth 2.0 JWT Bearer Flow for Server-to-Server Integration
Need a web developer in Nashville?
PHP, WordPress and Next.js. Free quote, and I usually reply within a couple of hours.
caleb@calebdorris.com and call or text (615) 914-1073