Skip to main content

Named Credentials and External Credentials in Salesforce: where callout secrets belong

Every Salesforce org that talks to another system has to keep a secret somewhere: an API key, a client secret, a password, a private key. Where that secret lives decides who can read it, who can use it, and what happens when it leaks. Salesforce's answer is Named Credentials and, since Winter '23, External Credentials. Used well, the secret never appears in Apex, never lands in Git and never shows up in a debug log. Used badly, it is a password sitting in deployable metadata.

This post covers how the two objects split the job, the permission set mapping that trips up most teams, what to do about legacy Named Credentials, and the mistakes we see most often in real orgs.

Two objects, two jobs

The current model separates where from how:

  • A Named Credential holds the endpoint: the base URL and callout options.
  • An External Credential holds the authentication: the protocol (for example OAuth 2.0, JWT, AWS Signature V4, or Custom for headers you build yourself), and one or more principals that carry the actual secret values.

A principal is either a named principal (one credential shared by everyone allowed to use it, the usual shape for system-to-system integrations) or a per-user principal (each Salesforce user stores their own credential for the external system). Salesforce describes the parts in its named credentials help and the Winter '23 announcement.

The split matters for security because the endpoint is safe to deploy and review, while the secret is set in each org and stays there.

The permission set mapping

External Credentials authenticate; permission sets authorize. A user can only make a callout through an External Credential if a permission set (or profile) they hold grants access to one of its principals, under External Credential Principal Access.

This is the step most teams miss on the first deploy, and it is also the step that decides your blast radius. Two rules of thumb:

  1. Map principals to a dedicated permission set, not to a broad one that half the org already holds. Whoever holds that permission set can make the callout as the principal.
  2. Remember per-user tokens are records. Per-user principals store each user's credential in the User External Credential object, so users need access to it. Grant that access deliberately on custom profiles rather than copying a permission set that grants far more.

If the integration runs from a scheduled job or a trigger, map the principal to the permission set held by the running user, not to every user who might cause the trigger to fire. Note that a trigger runs as the user who saved the record, so that user needs principal access too.

Calling it from Apex

Here is the pattern we still find in older code: a raw URL, a key read from a custom setting, and a header built by hand.

// Before: key in a custom setting, endpoint in Remote Site Settings
HttpRequest req = new HttpRequest();
req.setEndpoint('https://api.example.com/v2/orders');
req.setHeader('X-Api-Key', Integration_Settings__c.getOrgDefaults().Api_Key__c);
req.setMethod('GET');
System.debug('Calling with ' + req.getHeader('X-Api-Key'));
HttpResponse res = new Http().send(req);

Anyone who can read the custom setting can read the key, and the debug line writes it to every log. The fixed version names the credential and lets the platform add the header:

// After: endpoint and secret both resolved by the platform
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Orders_API/v2/orders');
req.setMethod('GET');
HttpResponse res = new Http().send(req);

For an API key in a custom header, define the header on the External Credential (or the Named Credential) with a formula such as {!$Credential.Orders_Ext.ApiKey}, and turn on Allow Formulas in HTTP Header on the Named Credential. When you build the Authorization header yourself, turn off Generate Authorization Header so the platform does not add a second one. A callout through callout: also needs no Remote Site Setting.

Legacy Named Credentials

Before Winter '23, a Named Credential held both the URL and the secret in one record. Salesforce now calls these legacy Named Credentials, marks them deprecated, and says they will be discontinued in a future release. They still work, and many orgs have dozens.

Migration is mostly mechanical: create an External Credential with the same protocol, move the secret into a principal, point a new Named Credential at it, add the principal access mapping, then switch the callout: name in code. The step that changes behaviour is the mapping: a legacy credential could be used by anyone whose code referenced it, while the new model needs a permission set. Test with the real running user, not an admin.

Common mistakes

Mistake Why it matters Fix
<password> in *.namedCredential-meta.xml Deploys the secret to every target org and stores it in Git history Remove it; set the secret in each org after deploy
http:// endpoint The request and any credential travel in cleartext Use https://
Merge fields allowed in the body User-influenced values can be interpolated into a callout template Leave Allow Formulas in HTTP Body off; build bodies in Apex with explicit encoding
Principal mapped to a broad permission set Everyone who holds it can make the callout A dedicated permission set for the integration
Secret in a custom setting or Custom Metadata Readable by anyone with access to the setting, and often deployed Move it into an External Credential principal
Logging the request Headers and tokens end up in debug logs Never log Authorization or key headers
Credentials never rotated A leaked secret keeps working Rotate on a schedule and after staff or vendor changes

The first row deserves emphasis. A secret committed to a repository is still in history after you delete the line; see secrets in git history for cleanup.

How Vulkro for Salesforce checks this

Vulkro for Salesforce reads every Named Credential file in source and reports a hardcoded <password> (Critical), a cleartext endpoint (High), allowMergeFieldsInBody=true (Medium) and an anonymous principal paired with a baked-in credential (High). A secret-like value in an External Credential is reported as externalcredential-hardcoded-secret. Details are in the Named Credentials docs.

Settings changed in Setup never reach source, so the live-org check reads them directly:

vulkro-sf org named-credentials --target-org my-prod

It reports SF-NC-LIVE-001 (an http:// endpoint), SF-NC-LIVE-002 (an anonymous principal paired with an auth provider) and SF-NC-LIVE-003 (merge fields allowed in the body), and never reads or prints secret values. vulkro-sf org governance also flags named and external credentials unchanged for a year as a rotation reminder.

FAQ

What is the difference between a Named Credential and an External Credential?

The Named Credential holds the endpoint URL and callout options. The External Credential holds the authentication protocol and the principals that carry the secret. One External Credential can back several Named Credentials.

Why does my callout fail after deploying an External Credential?

Most often the running user has no permission set granting External Credential Principal Access to the principal. Add the principal to a dedicated permission set and assign it to the user who actually runs the code.

Should I migrate legacy Named Credentials?

Yes. Salesforce has deprecated them and says they will be discontinued in a future release. Migrating also moves you to explicit, permission-set-based access to the secret.

See Vulkro for Salesforce and Start a 14-day trial. Related: connected apps and OAuth tokens and hardcoded secrets in private repos.