SfSkills Domains Docs GitHub

Home / apex / apex-named-credentials-patterns

apex-named-credentials-patterns

Use when writing Apex that calls out to external endpoints via Named Credentials, working with custom header formula tokens ({!$Credential.OAuthToken}), querying per-user auth state through the UserExternalCredential SObject, or diagnosing why Named Credential callouts fail.

Open the skill folder on GitHub


Apex Named Credentials Patterns

Use this skill when writing Apex that makes authenticated outbound callouts through Named Credentials — covering the callout: URL prefix, the {!$Credential.*} merge fields for injecting credential values into headers and bodies, the ExternalCredential / NamedCredential / PermissionSet metadata a developer actually deploys, and the behavioral differences between the legacy and SecuredEndpoint models that affect Apex code.

Boundary: integration/named-credentials-setup owns the declarative security design. This skill owns how Apex consumes it and the metadata shapes a developer deploys alongside the code.


Before Starting


Questions to Ask Before Configuring

AskWhy it mattersWhat a good answer adds
What is the Named Credential's API name, and does a credential with that exact name exist in every org you will deploy to?callout: resolves by API name. Same-name credentials with different URL parameters are the platform's own portability mechanism (apexdev L34327–34331) — the reason callout code never needs an environment check.You get one Apex class that deploys unchanged to scratch, sandbox and production, and a deploy-time check the checker can enforce (NC-APEX-003).
Is the credential Legacy or SecuredEndpoint?Every field the legacy shape relies on is deprecated at API 56.0 (api_meta L89950–90240). Building new Apex on it means building on a deprecated surface, and hand-authored files mix the two shapes silently. See gotchas 1 and 5.You write the right XML the first time instead of deploying a SecuredEndpoint credential whose URL sits in a legacy-only <endpoint> element and is therefore ignored.
Named principal or per-user principal?NamedPrincipal shares one identity; PerUserPrincipal gives per-user access control (api_meta L63806–63808). Only the named principal works for Batch, Queueable and Scheduled Apex. It also changes what admin edits cost — see gotcha 10.The async design is settled before the code is written, and the external system's audit trail is a deliberate choice rather than a side effect.
Which permission set grants the principal, and is it assigned to the user the callout runs as?A principal is inert until externalCredentialPrincipalAccesses grants it (api_meta L94794–94796). Nothing in the deploy fails without it. See gotcha 6 and anti-pattern 8.The change set includes the grant and the assignment step, so the integration works on first run instead of returning 401 for every user.
Who owns the Authorization header — the platform or your Apex?generateAuthorizationHeader defaults to true (api_meta L90039–90046). Apex adding its own on top is a collision that surfaces as a 401. See gotcha 4.One owner, encoded in metadata that travels with the code, instead of an implicit default nobody deployed.
Does the endpoint need a credential value in a specific header name or in the body?That is what {!$Credential.*} merge fields are for, and they need allowMergeFieldsInHeader / allowMergeFieldsInBody, both defaulting to false (api_meta L89914–89940). See gotchas 2 and 3.The flags ship with the Apex that depends on them, so the endpoint receives the credential rather than the literal merge-field text.
What is the endpoint URL — host and path — and do sandbox and production share a host?A SecuredEndpoint credential's Url parameter needs a real value before deploy, and it is one of the two facts an AuthHeader formula depends on. Proven live in a dry-run (sf project deploy start --dry-run, API 67.0), not stated in the guide. See gotcha 14.The NamedCredential deploys with a real endpoint on the first attempt, and sandbox-vs-production drift is a decision instead of a placeholder left behind.
What is the name of the authentication parameter the header formula will merge in (e.g. ApiKey, ApiToken), and who enters its value in Setup after deploy?An AuthHeader parameter needs a non-empty parameterValue, or the deploy fails with The parameter type "AuthHeader" requires these fields: ParameterValue. — and a PermissionSet naming that credential's principal fails in the same request. Proven live in a dry-run, not stated in the guide. See gotcha 14.The ExternalCredential deploys with the {!$Credential.<EC>.<ParameterName>} formula in place, and the post-deploy Setup step that creates the actual parameter is scheduled instead of discovered at deploy time.
What kind of transaction makes the callout — synchronous, Queueable, Batch, or a Continuation?Timeout budget, principal choice and framework support all follow from this. Continuations do accept callout: endpoints; the documented exclusion is Private Connect (apexdev L36006–36070). See gotcha 11.The right timeout and the right mocking API (Test.setMock vs Test.setContinuationResponse) are chosen up front rather than discovered in a failing test.

What a proper configuration adds over just doing it: a hardcoded endpoint and a concatenated Authorization header will call the API successfully today — what the credential model buys is that the secret is never in source or in an export, the endpoint changes per org without a code change, and access is granted and revoked with the same permission sets that govern everything else.


Core Concepts

Legacy vs. SecuredEndpoint Named Credential Model

The legacy model combines the endpoint URL and authentication configuration in a single Named Credential metadata record. The enhanced model (namedCredentialType SecuredEndpoint, Spring '22+) separates them:

From Apex code, both models use exactly the same callout: URL syntax. The difference is invisible in ordinary callout code and highly visible in the metadata you deploy. namedCredentialType valid values are Legacy, PrivateEndpoint, SecuredEndpoint, and Standard (reserved for internal use), available in API version 56.0 and later (api_meta L90140–90152).

ConcernLegacySecuredEndpoint
Endpoint URL<endpoint> (deprecated at 56.0)namedCredentialParameters with parameterType Url
Auth protocol<protocol> (deprecated at 56.0)ExternalCredential.authenticationProtocol
Identity<principalType> (deprecated at 56.0)ExternalCredentialParameter NamedPrincipal / PerUserPrincipal
Who may use itProfile / user-level credential entryPermissionSet.externalCredentialPrincipalAccesses (API 59.0+)
Files to deploy13 (EC, NC, permission set)

The callout: URL Prefix

"A named credential URL contains the scheme callout:, the name of the named credential, and an optional path. For example: callout:My_Named_Credential/some_path" (apexdev L34332–34334). A query string is appended with ?: callout:My_Named_Credential/some_path?format=json (apexdev L34335–34337).

HttpRequest req = new HttpRequest();
req.setEndpoint('callout:MyServiceNC/api/v2/customers');
req.setMethod('GET');
HttpResponse res = new Http().send(req);

The name is the Named Credential's API name, never the External Credential's. The platform resolves the endpoint, applies authentication, and skips the Remote Site Settings requirement for that host (apexdev L34319–34322).

{!$Credential.*} Merge Fields — an Apex-Side Feature

The Apex Developer Guide's section is titled "Merge Fields for Apex Callouts That Use Named Credentials", and its examples are Apex, not Setup (apexdev L34456–34513):

req.setHeader('X-Username', '{!$Credential.Username}');
req.setHeader('Authorization', '{!$Credential.OAuthToken}');
req.setBody('Password:{!HTMLENCODE($Credential.Password)}');
Merge fieldResolves toAvailable when
{!$Credential.Username} / {!$Credential.Password}Username / password of the running userPassword authentication
{!$Credential.OAuthToken}OAuth token of the running userOAuth authentication
{!$Credential.AuthorizationMethod}Basic, Bearer, or nullDepends on protocol
{!$Credential.AuthorizationHeaderValue}Base-64 user:pass, OAuth token, or nullDepends on protocol
{!$Credential.OAuthConsumerKey}Consumer keyOAuth authentication

Two flags gate them, both defaulting to false: allowMergeFieldsInHeader and allowMergeFieldsInBody (api_meta L89914–89940). HTMLENCODE is the only formula function permitted around a merge field, and only in bodies (apexdev L34508–34513). Apex never sees the resolved value — the substitution happens on the platform side at callout time.

Principals and Who May Use Them

ExternalCredentialParameter.parameterType NamedPrincipal means "the parameter uses the same set of user credentials for all users who access the external system"; PerUserPrincipal "provides access control at the individual user level" (api_meta L63806–63808). Access is granted from the permission set side:

<externalCredentialPrincipalAccesses>
    <enabled>true</enabled>
    <externalCredentialPrincipal>Partner_Orders_EC-PartnerOrdersNamedPrincipal</externalCredentialPrincipal>
</externalCredentialPrincipalAccesses>

The value is the External Credential name and the principal name joined by a dash (api_meta L94995–94999), available in API version 59.0 and later. Before 58.0 the link ran the other way, from ExternalCredentialParameter.principal to a permission set — that field "is removed in API version 58.0 and later" (api_meta L63830–63831).

For a per-user principal, Apex may want to know whether the running user has authenticated before it calls. Treat that gate as an optimisation, not a contract: the UserExternalCredential and ExternalCredential objects are not documented in the Object Reference used to ground this skill, so run sf sobject describe against the target org before coding against a field list (gotcha 12). Handle 401 and 403 in the callout path regardless.

Callout Limits That Bite Named Credential Code

Deeper coverage of the limits themselves belongs to integration/callout-limits-and-async-patterns.


Common Patterns

Environment-Portable Callout Through a SecuredEndpoint Credential

When to use: the default. One Apex class deploys unchanged everywhere; each org's credential carries its own URL parameter under the same API name.

How it works: the Apex names the credential and a path; the platform supplies the base URL, the authentication, and the Remote Site exemption. Build on templates/apex/HttpClient.cls, which composes 'callout:' + namedCredential + path for you.

HttpClient.Response res = new HttpClient()
    .namedCredential('Partner_Orders_NC')
    .path('/v2/orders/' + EncodingUtil.urlEncode(externalId, 'UTF-8'))
    .method('GET')
    .timeoutMs(20000)
    .retryOnTransient(false)   // HttpClient's backoff is a busy-wait; retry from a Queueable
    .send();

Why not the alternative: an if (isSandbox) branch or a Custom Setting holding the base URL reimplements, worse, the portability the platform already provides — and gives you a second place for production and sandbox to drift apart.

Endpoint That Needs the Credential in a Non-Standard Header

When to use: the API wants the token in X-Api-Key, or wants Authorization in a shape the platform's generated header does not match.

How it works: turn off the platform's header, turn on header merge fields, and write the header in Apex.

// Named Credential: generateAuthorizationHeader = false, allowMergeFieldsInHeader = true
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Partner_Orders_NC/v2/orders');
req.setHeader('Authorization', 'Bearer {!$Credential.Password}');
req.setHeader('Accept', 'application/json');

Why not the alternative: reading a token from a Custom Setting or Custom Label to build the same header puts the secret in the database, outside the credential vault, deployable to any sandbox without rotation, and visible in an export.

Replacing a Remote Site Setting Plus a Hardcoded Secret

When to use: an inherited integration that calls a literal URL with a concatenated Authorization header.

How it works: deploy External Credential → Named Credential → permission set, change one line of Apex, delete the Remote Site Setting last.

// Before
req.setEndpoint('https://api.acme-corp.com/v2/orders/' + id);
req.setHeader('Authorization', 'Bearer ' + System.Label.Acme_Token);

// After — endpoint and auth both move out of source
req.setEndpoint('callout:Acme_Orders_NC/v2/orders/' + id);

Why not the alternative: leaving the Remote Site Setting in place after the migration hides the fact that some other code path still calls the host directly — the checker's NC-RSS-001 advisory exists to surface exactly that.


Decision Guidance

SituationRecommended ApproachReason
Standard REST callout to an authenticated endpointcallout:NCName/path in setEndpoint()Platform resolves endpoint, applies auth, skips Remote Site Settings
Token must appear in a custom header namegenerateAuthorizationHeader false + allowMergeFieldsInHeader true + setHeader with {!$Credential.*}The merge field resolves platform-side; the secret never enters Apex
Credential value needed inside the request bodyallowMergeFieldsInBody true + {!HTMLENCODE($Credential.Password)}HTMLENCODE is the only supported wrapper, and only in bodies
Long-running callout from a Visualforce pageContinuation with a callout: endpointNamed credential URLs are supported there (apexdev L36006–36018)
Same callout must traverse a private connectionnamedCredentialType PrivateEndpoint + QueueableAsync callouts are not supported over Private Connect
Async job (Batch / Queueable / Scheduled)NamedPrincipalThe running user is not the person who queued the work
External system must attribute actions to individualsPerUserPrincipal + 401/403 re-auth pathPer-user access control at the cost of an onboarding flow
Managed package calling a subscriber's credentialAdd AllowedManagedPackageNamespaces to the credentialNamespaced packages are named explicitly on the credential
Inherited legacy credential still workingLeave it, plan the migrationEvery legacy field is deprecated at 56.0 — new work goes on SecuredEndpoint

  1. Identify the credential and its model. Get the Named Credential API name from the requester, then confirm the shape: sf data query --query "SELECT DeveloperName, CalloutOptionsGenerateAuthorizationHeader, CalloutOptionsAllowMergeFieldsInHeader FROM NamedCredential". A populated Endpoint or PrincipalType means legacy (both are legacy-only and deprecated at 56.0). Work through the Questions to Ask table before writing anything.
  2. Author or amend the metadata. Follow references/code-examples.md sections 1–3 for the ExternalCredential, NamedCredential and PermissionSet XML. Settle the two decisions the Apex depends on here, not later: who owns the Authorization header (generateAuthorizationHeader), and whether merge fields are needed (allowMergeFieldsInHeader / allowMergeFieldsInBody).
  3. Write the Apex against templates/apex/HttpClient.cls. Name the credential, set an explicit timeout, and branch on 401/403 separately from other non-2xx codes so an access failure reads as an access failure. Section 4 of references/code-examples.md is the reference implementation.
  4. Write the test with templates/apex/tests/MockHttpResponseGenerator.cls. Cover the success shape, the endpoint's not-found code, and the 401 path. Do not assert on req.getEndpoint() inside the mock — route on the path substring instead (section 6 explains why).
  5. Run the checker. python3 scripts/check_apex_named_credentials_patterns.py --manifest-dir force-app --strict. It resolves every callout: name against the credentials in the tree, flags a legacy namedCredentialType, catches a SecuredEndpoint credential with no externalCredential link, and reports an Authorization header that collides with the platform's.
  6. Deploy in dependency order and verify. External Credential (with its principal) deploys first, then Named Credential, then the permission set, then Apex. Only after the External Credential deploys does a human enter the API key against the principal in Setup — never in metadata — and only then does sf org assign permset run. A validate-only deploy (--dry-run / checkOnly) of the credential can pass with the AuthHeader formula in place while that Setup value is still empty; the deploy checks the formula's shape, not the secret behind it, so the runtime 401 on a live callout is the real UAT check, not the green deploy (gotcha 14). Verify with the NamedCredential SOQL in section 8 and sf apex run test.
  7. Retire what the credential replaced. Delete the Remote Site Setting and the Custom Label or Custom Setting that held the old secret, and rotate the secret — it was in source control, so it is compromised.

Review Checklist


Salesforce-Specific Gotchas

Full detail, with line citations, in references/gotchas.md. The six that most often break a first deployment:

  1. A SecuredEndpoint credential's URL belongs in a Url parameter. <endpoint> is legacy-only and deprecated at API 56.0, so a copied file can carry a URL the modern type ignores.
  2. generateAuthorizationHeader is true unless you deploy false. Apex that adds its own Authorization on top collides with the platform's.
  3. Merge fields are Apex-side, but inert until the flags are on. Both allowMergeFields* fields default to false; the literal text reaches the endpoint instead of the credential value.
  4. The principal grant lives on the permission set from API 59.0. ExternalCredentialParameter.principal was removed at 58.0; without externalCredentialPrincipalAccesses every callout is unauthorized.
  5. Continuations do support callout:. The widely repeated incompatibility is not in the guide; the documented exclusion is Private Connect.
  6. An AuthHeader parameter with no parameterValue fails deploy validation. Proven live in a dry-run, not stated in the guide — and a permission set naming that credential's principal fails in the same deploy as a cascade. See gotcha 14.

Output Artifacts

ArtifactDescription
ExternalCredential XMLauthenticationProtocol, a NamedPrincipal or PerUserPrincipal parameter, and any AuthHeader / AuthParameter entries
NamedCredential XMLSecuredEndpoint with Url and Authentication parameters, plus explicit generateAuthorizationHeader and allowMergeFields* flags
PermissionSet fragmentexternalCredentialPrincipalAccesses granting <EC>-<principal>
Apex client class + -meta.xmlcallout: endpoint via templates/apex/HttpClient.cls, explicit timeout, 401/403 branch
Apex test classMockHttpResponseGenerator covering success, not-found and 401
package.xml + deploy orderEC → NC → PermissionSet → Apex, with the manual credential-entry and assignment steps called out
Checker reportNC-APEX-*, NC-META-*, NC-XREF-*, NC-PERM-*, NC-RSS-*, NC-AUTH-*, NC-PS-* findings by severity

Reference Files

FileRead it when
references/code-examples.mdYou are writing the deployable artifacts: External Credential, Named Credential, permission set, Apex client, test, package.xml, deploy order, and verification queries
references/gotchas.mdA callout fails for a reason the code does not explain, or you are hand-authoring credential XML
references/llm-anti-patterns.mdReviewing generated Apex or credential XML, or self-checking your own output
references/examples.mdYou want a worked scenario end to end, including the before/after of a migration
references/well-architected.mdChoosing between the legacy and SecuredEndpoint models, or between principal types, and needing the tradeoff written down

Package files