Skip to content
Febrian Tarigan

The SOAP Call Your Analytics Pipeline Still Depends On

· 7 min · salesforce / integration / authentication

Every Salesforce org I’ve spent real time in eventually turns up the same three lines of code somewhere in its analytics or middlewares stack:

from simple_salesforce import Salesforce
sf = Salesforce(username='...', password='...', security_token='...')

It shows up in a nightly ETL job, in a notebook someone wrote for a one-off report that quietly became load-bearing, in a scheduled script nobody’s touched. It’s the first thing that worked, and it kept working, so it stayed.

What that constructor is actually doing under the hood is calling the SOAP API’s login() method. Username, password, and security token go in, a session ID and server URL come back, and every request after that rides on that session ID. It’s the oldest, simplest way to authenticate against Salesforce, which is exactly why so much still depends on it.

Why it’s going away#

Salesforce has been narrowing the password-based paths into its API for several release cycles, and SOAP login() is next. Per Salesforce’s own Platform SOAP API login() Retirement notice, the call is being retired for API versions 31.0 through 64.0 with the Summer ‘27 release, and it’s already unavailable in API 65.0 and later. Newly created orgs ship with it disabled by default, and starting Summer ‘26 those orgs also require the “Use Any API Auth” permission before it can be used at all, even once an admin turns it back on. If your org has already received the email titled “ACTION REQUIRED: Update Unsupported Platform SOAP API login(),” Salesforce has already found the specific integrations that will break.

That’s a fixed deadline now, not a someday. A username and password, plus a security token you reset by ignoring an email until you need it, isn’t a credential you can scope, rotate, or revoke independently of the person it belongs to. If an integration built this way is compromised, whoever’s on the other end has exactly the access that person has. There’s no audit trail that tells “logged in from her laptop” apart from “the nightly export job logged in as her.”

The security token doesn’t help as much as it looks like it should, either. It’s a static string Salesforce mails you until you reset it, most people reset it exactly once, when they set the integration up, and it ends up pasted into a ticket or a Slack thread the first time someone’s login starts failing and nobody remembers why. It’s a second factor in name, not in practice.

None of this is Salesforce being unusually strict. Password-based API access has been quietly going away across the industry for a few years. Google deprecated basic auth for Gmail’s API, Microsoft did the same across its Graph API, and the pattern is always identical: password-based access looks convenient right up until someone has to explain, during an incident, exactly which system was using which shared credential.

The part that catches people#

simple-salesforce isn’t a neglected tool nobody maintains. It’s actively maintained and genuinely well designed, which is precisely why it’s the fastest way to get a Python script talking to Salesforce, and precisely why it ends up everywhere an analytics or data team touches the platform.

Part of why the shortcut wins is that the alternative used to have real friction. Registering a Connected App, working out scopes, handling a refresh token: that’s an afternoon of setup for something a data engineer wants to be a five-minute pip install in a notebook. Logging in with a username, password, and security token was never the wrong instinct. It solved a real problem quickly. It just solved it in a way that ages badly the moment more than one person’s job depends on that one shared credential.

The library isn’t the problem. Username-and-password as the first example in every tutorial and Stack Overflow answer is.

What actually breaks#

Authentication doesn’t degrade. It either succeeds or it doesn’t, so every job authenticating this way goes from working to entirely broken the moment the retirement takes effect, not gradually. If the job is the kind that pages someone on failure, you find out fast. If it isn’t, you find out days later when a report is empty or a dashboard goes stale, which is a worse way to learn about an outage than an alert at 2am.

What to switch to#

Salesforce’s own guidance points at external client apps and OAuth, and there are two real ways to get there depending on how much you want to change today.

The fastest one keeps things close to what you already have. Swap the security token for a client ID and secret from an External Client App (Salesforce’s newer replacement for what used to be a Connected App), and leave username and password in place:

from simple_salesforce import Salesforce
sf = Salesforce(
username='myemail@example.com',
password='password',
consumer_key='consumer_key',
consumer_secret='consumer_secret',
)

That one change is enough to satisfy this specific retirement, since it authenticates through the connected app’s OAuth credentials instead of a bare SOAP login. [TK: confirm whether this specifically hits the OAuth token endpoint or another mechanism, if you want to state the exact path with certainty.] It’s close to a config change, not a rewrite, which is exactly why it’s the migration most teams will reach for first.

It’s worth being honest about what that fix doesn’t solve. A username and password are still going over the wire on every authentication, just through a different door. You’ve satisfied this specific retirement. You haven’t removed the thing this whole post opened with: a human’s password, live in a config file, as the reason an integration has access at all.

The fuller fix removes the password entirely, and there are two ways to do that. A true client credentials flow uses a client ID and secret standing in for a dedicated integration user, with no username or password anywhere in the config. The JWT bearer flow authenticates with a generated private key instead of a password, and can still impersonate a specific user when the job genuinely needs one.

Client credentials and JWT bearer solve different problems, and picking the wrong one just trades one kind of fragility for another. Client credentials authenticates as the integration itself, with no specific human behind it, which is right for batch jobs and ETL pipelines that shouldn’t be tied to any one person’s account. If a job genuinely needs to act as a specific user, because record ownership or sharing rules depend on who’s making the change, JWT bearer is the one that fits: it lets an integration impersonate a named user without that user’s password ever existing in the integration’s config.

Test whichever flow you land on in a sandbox before the old one stops working, not after. A migration that only gets exercised for the first time under deadline pressure, against production, is how a clean auth swap turns into an actual outage.

Setting up the External Client App itself is the fast part, maybe twenty minutes: create it, assign it a scoped permission set instead of a real user’s full access, generate the secret, and store it somewhere that isn’t a .env file that ends up committed to git by accident. The permission set is the part worth doing properly. A dedicated integration user with exactly the object and field access the job needs is a much smaller blast radius than whatever a real employee happens to have this quarter.

The ones you won’t find by searching#

The integrations your team owns are the easy part. The ones that cost real time are the ones nobody remembers building: a Heroku app from years ago, a BI tool whose built-in Salesforce connector only offers username and password as an option, a consultant’s script that ran once and got cron-scheduled by someone who’s since left. Finding those is mostly grepping every place your org keeps credentials, and even that won’t catch the one running from someone’s laptop.

If you haven’t started, the fastest way in is usually: