If you’ve been using Salesforce Named Credentials for callouts, the authentication model changed significantly starting in API version 53. The original Named Credential crammed both the endpoint URL and the authentication credentials into one object. The new model separates them: an External Credential holds the authentication configuration, and a Named Credential references it. This post covers why the change was necessary, how the new model works in practice, and what a migration looks like.
The Problem with the Old Model
The original Named Credential design conflated two distinct concerns:
- Where to call - the endpoint URL
- How to authenticate - credentials, tokens, OAuth config
This caused real problems:
- OAuth credentials were stored at the org level, so all users making callouts shared the same identity. There was no way to make callouts on behalf of the current user without custom token management.
- Password-based auth meant usernames and passwords lived in Salesforce metadata. Rotating them required updating the Named Credential and redeploying.
- There was no principled way to manage per-user authentication for systems that required individual OAuth consent flows.
The New Architecture
The new model introduces a clear separation:
External Credential
└── Principal (Per-Org or Per-User)
└── Authentication Protocol (OAuth, JWT, Custom)
└── Parameters (client_id, client_secret, token URL, etc.)
Named Credential
└── References External Credential
└── URL
└── Callout Options
External Credential - stores the authentication scheme. Think of it as the “how to authenticate” object. It defines whether auth is per-org or per-user, which OAuth flow to use, and holds the credential parameters (ideally referencing Named Principal parameters rather than hardcoding secrets).
Named Credential - stores the endpoint URL and references an External Credential. This is the object you reference in Apex.
Per-User vs Per-Org Principals
The principal type is the most important design decision:
Per-Org principal - a single identity is used for all callouts, regardless of which Salesforce user initiates them. Appropriate for system-to-system integrations where the calling service has one identity (e.g., syncing data to a warehouse).
Per-User principal - each Salesforce user has their own OAuth token. When a user makes a callout, it authenticates as that user in the external system. Requires each user to complete an OAuth authorization flow before making their first callout.
Setting Up OAuth 2.0 Client Credentials Flow
The client credentials flow is the right choice for server-to-server integrations where there’s no user involved in the auth handshake.
In Setup:
- Navigate to Named Credentials → External Credentials → New
- Set Authentication Protocol to OAuth 2.0
- Set Identity Type to Named Principal
- Add parameters:
Authentication URL:https://login.example.com/oauth/tokenClient ID: your app’s client IDClient Secret: your app’s client secret (mark as Protected)Scope: the required OAuth scopes
Then create a Named Credential that references this External Credential:
- Named Credentials → New
- Set the URL to your base endpoint (e.g.,
https://api.example.com) - Set External Credential to the one you just created
- Enable Allow Formulas in HTTP Body if needed
Apex Callout Pattern
Nothing changes in how you write Apex callouts - the callout: prefix still works:
public class ExampleApiClient {
public static String fetchData(String resourcePath) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MyExternalAPI' + resourcePath);
req.setMethod('GET');
req.setHeader('Accept', 'application/json');
req.setTimeout(10000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return res.getBody();
} else {
throw new CalloutException(
'API call failed: ' + res.getStatusCode() + ' ' + res.getStatus()
);
}
}
public static Map<String, Object> createRecord(Map<String, Object> payload) {
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MyExternalAPI/records');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setBody(JSON.serialize(payload));
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 201) {
return (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
} else {
Map<String, Object> err = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
throw new CalloutException('Failed to create record: ' + err.get('message'));
}
}
}
The callout:MyExternalAPI prefix maps to the Named Credential’s API name. The path appended after it is concatenated to the Named Credential’s base URL.
Migrating Legacy Named Credentials
If you have existing Named Credentials using the legacy model (pre-API 53 style with password or OAuth directly in the Named Credential), the migration path is:
- Create an External Credential with the same auth configuration that’s currently embedded in your Named Credential
- Create a new Named Credential that references the External Credential - use a new API name temporarily
- Test callouts through the new Named Credential in a sandbox
- Update Apex code to reference the new Named Credential name (a straightforward find-and-replace)
- Delete the old Named Credential once all references are updated
The legacy Named Credentials still work - Salesforce hasn’t removed them - but new integrations should always use the split model. The separation makes credential rotation, per-user OAuth, and security auditing significantly more manageable.
What This Means for Deployments
External Credentials and their parameters can be deployed via metadata, but Protected parameters (secrets) are excluded from deployments by design. This means you’ll need a post-deployment step to populate credential values in each environment. Document this in your deployment runbook - it’s a common gap that causes production incidents when someone deploys to a new org and forgets to configure the external credential secrets.