A few months ago, a client called us with some uncomfortable news - one of their developers was leaving the company. Nothing unusual, until we found out he was the only person who knew the password for a critical API. Not because he was keeping secrets - but because the password was hardcoded directly in the code. In five different files.
This is not an isolated case. We see it constantly.
Integration security is one of those topics that gets pushed back - "we'll fix it later" - until "later" turns into an incident. Here are the three mistakes we run into most often in Salesforce integrations, and how to avoid them.
Mistake #1: Hardcoded Passwords and API Keys in Apex Code
It sounds obvious, but it happens more than you'd think:
// Do this instead
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_External_Service/api/endpoint');
req.setMethod('POST');
// Authentication details stay out of the Apex code
The problem is not just that someone might read the code - the problem is that the code ends up in version control, in deployments, in sandbox environments. The password is no longer a secret. It's commit history.
The Fix: Named Credentials
Named Credentials, together with External Credentials, are Salesforce's built-in way to manage external endpoints and authentication. Instead of storing URLs and credentials in code, you configure them once in Setup and reference the Named Credential from Apex:
// Do this instead
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:My_External_Service/api/endpoint');
req.setMethod('POST');
// Authentication details stay out of the Apex code
You configure this under Setup > Security > Named Credentials. Note that the screen has two tabs - Named Credentials and External Credentials - and you generally need a record on both. The endpoint lives on the Named Credential, while the authentication lives on a linked External Credential. Between the two you define things like:
- Label - a developer-friendly display name (Named Credential)
- Name / API Name - the value Apex uses after
callout: (Named Credential)
- URL - the base endpoint (Named Credential)
- Authentication Protocol - OAuth 2.0, AWS Signature Version 4, Password Authentication, or Custom (External Credential). JWT is not a separate protocol - it is an OAuth 2.0 flow (JWT Bearer Token) configured on top of OAuth 2.0.
- Principal - Named Principal or Per-User Principal (External Credential)
That API name detail matters. If the Named Credential label is "My External Service" but the name is My_External_Service, Apex uses:
req.setEndpoint('callout:My_External_Service/api/endpoint');
Once configured, Salesforce keeps the authentication details out of Apex and can pass the right token or header during the callout. You write less code and there are no credentials floating around in your repository.
Mistake #2: Storing Deployment Configuration in Custom Settings
Many teams use Custom Settings for configuration - feature flags, environment-specific paths, timeout values, retry counts. It works. But there is one significant trap: the Custom Setting definition can be deployed, but the actual Custom Setting records are data. They do not travel through Change Sets or metadata deployments the same way Custom Metadata records do.
If you have Custom_Setting__c.API_Timeout__c = 30000 in production and forget to set it in a new sandbox, the integration can fail at runtime in a non-obvious way. No clear setup clue, no obvious cause - just broken behaviour in an environment that was supposed to match production.
The Fix: Custom Metadata Types
Custom Metadata Types are proper metadata records - they deploy with your code, can be included in packages, and travel through your CI/CD pipeline like any other metadata.
For authenticated callouts, keep the base endpoint and authentication in the Named Credential. Use Custom Metadata for the deployable, non-secret parts of the integration: which Named Credential to use, which path to call, timeout values, retry attempts, feature flags, or routing rules.
// Reading deployable configuration from Custom Metadata
Integration_Config__mdt config = [
SELECT Named_Credential__c, Endpoint_Path__c, Timeout_ms__c, Retry_Attempts__c
FROM Integration_Config__mdt
WHERE DeveloperName = 'Payment_Gateway'
LIMIT 1
];
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:' + config.Named_Credential__c + config.Endpoint_Path__c);
req.setTimeout(config.Timeout_ms__c.intValue());
In this example, Named_Credential__c stores something like Payment_Gateway_API, and Endpoint_Path__c stores something like /payments/charge. The actual base URL and authentication still live in the Named Credential and External Credential.
A quick footgun to watch for: Apex callout timeouts are capped at 120,000 ms (2 minutes). If someone stores a larger value in the Custom Metadata record, setTimeout will throw at runtime. Validate the value at deploy time or clamp it in Apex.
No hardcoded secrets. No deployment surprises. And if you need to change the retry count, timeout, or endpoint path for the payment integration, you update a metadata record - you do not touch the Apex code.
> Note: Custom Metadata is for configuration, not for secrets. The values are visible to admins and users with the relevant Setup or metadata access. (Field-level security on Custom Metadata fields and protected Custom Metadata records narrow this down, but neither makes the value a secret - for actual credentials, refer back to mistake #1.)
Mistake #3: Overly Permissive Profiles for Integration Users
When connecting an external system to Salesforce, teams often create an integration user - a dedicated Salesforce user for inbound API calls, webhooks, or automated processes. For outbound callouts, access to an External Credential is granted through a Permission Set with External Credential Principal Access enabled for the relevant principal - that is what controls which Salesforce users can use the external authentication. (The Named Credential itself does not carry user access; the access lives on the External Credential.) The common mistake is the same in both cases: giving too much access because it's "easier" and "guaranteed to work."
That is the opposite of secure.
If the integration user is compromised, the attacker gets far more access than they should. If the integration itself has a bug, it can accidentally delete or modify data it was never supposed to touch.
The Fix: Principle of Least Privilege
Start from a restricted base profile, such as Minimum Access - Salesforce or a tightly controlled clone of it. Then grant only the access the integration needs through Permission Sets or Permission Set Groups:
```
Integration User - Payment Gateway
Base Profile
Minimum Access - Salesforce (or a restricted clone)
Permission Set Group
Payment Gateway Integration
Object Permissions
Order__c: Read, Edit
Payment__c: Read, Create
(everything else: no access)
Field Permissions
Payment__c.Amount__c: Read, Edit
Payment__c.Status__c: Read, Edit
Payment__c.Card_Number__c: no access
Apex Class Access
PaymentWebhookHandler: Enabled
External Credential Principal Access
Only the external credential this integration uses
Avoid using the System Administrator profile for automated processes. Also watch for broad permissions such as View All Data and Modify All Data. They override normal sharing controls and can turn a small integration bug into an org-wide incident.
It takes a bit more time to set up. It saves a lot of pain when something goes wrong.
What We Have Learned
- Named Credentials - the default Salesforce-native place for authenticated callout endpoints and credentials. Apex references the Named Credential name/API name with
callout:...; credentials stay out of code, Custom Settings, and "temporary" config files.
- Custom Metadata Types - for deployment configuration that needs to travel with your code. Use it for non-secret settings like paths, timeouts, retry counts, and routing rules. Keep authenticated base endpoints and credentials in Named Credentials and External Credentials.
- Least privilege for integration users - start from a restricted base profile, then grant specific access with Permission Sets or Permission Set Groups, including External Credential Principal Access for outbound callouts. System Administrator is not an appropriate default for automated processes.
Security is not a feature you add at the end. It is an architectural decision you make at the beginning - or pay for later.
Happy (secure) coding!