Day 2/15 — Headless 360 Architecture Explained
Every Salesforce Headless 360 (AIforce) request crosses the same five layers: client, identity, access, platform and response. Here is what happens at each one, who the request runs as, and how to choose between MCP and the REST API.

Arjun has been asked a simple question by his CIO: "If we let Neha's AI assistant into Salesforce, what exactly is connected to what?" He has a whiteboard, a marker and three proposals on the table. Neha wants Claude to brief her before her calls with Acme Global Tech. The web team wants a customer portal that shows Acme its open cases. And someone has already suggested "one integration user for all of it, to keep things simple".
Two of those proposals are fine. One of them is exactly the kind of shortcut that turns into a security review later, and the architecture is what tells you which one.
Yesterday was the map of Salesforce Headless 360 (now called AIforce). Today is the plumbing: what sits between a question typed into Claude and a row in the Opportunity table, and which parts of that path you control.
By the end of today you will be able to draw the path of one request through Headless 360, say who it runs as at every step, choose between the Model Context Protocol (MCP) and the REST API for a given job, and read your own org's API version and daily API allocation with one script.
Where we are
On Day 1 you learned what Headless 360 is, why it is now also called AIforce, and which parts are generally available. You also read a hosted MCP server's public metadata and saw its two scopes, mcp_api and refresh_token. Today we connect those pieces into one picture before Day 3 opens up the protocol itself.
The request path, end to end
Start with one concrete request. Neha types into Claude: "What's open on Acme Global Tech before my call with Priya?" Here is everything that has to happen before she gets an answer.

- Client. Claude is the host application. It holds a connection to a Salesforce hosted MCP server, reads the tools that server offers, and decides to call one, for example
soqlQuerywith a query it writes itself. - Identity. Before any of this worked, Neha signed in once. Her client sent her through an External Client App using OAuth 2.0 Authorization Code with PKCE, which Salesforce calls "mandatory for all OAuth flows" on hosted MCP. The result is an access token for Neha, a named user, with the
mcp_apiandrefresh_tokenscopes. - Access. The tool call goes to a URL such as
https://api.salesforce.com/platform/mcp/v1/platform/sobject-all. Note the host: hosted MCP endpoints live onapi.salesforce.com, not on your org's My Domain, which is where REST, GraphQL and UI API calls go. Salesforce requires JWT access tokens here, which "allows the MCP server to validate tokens without making a separate callout to Salesforce on every request". - Platform. The query runs inside your org as Neha. Salesforce lists four things "enforced on every API call, every MCP tool invocation, and every CLI command": identity, access, invocation scope and governance, where "Validation rules, triggers, approval chains, and governor limits all fire regardless of entry point." The call also counts against your org's daily API quota.
- Response. Records come back that Neha is allowed to see, and nothing else. If a tool changed a record, the audit trail names Neha as the editor. Claude turns the result into an answer.
Two details in that path are easy to miss. First, there is no language model on Salesforce's side of a standard hosted server. Salesforce's FAQ says "The processing of the MCP server is completely deterministic" and "The LLM is entirely on the client side". All the reasoning in steps 1 and 5 happens in Claude. Second, the model never touches Salesforce directly. It asks the host to call a tool, and the host sends a request that carries Neha's token. That is why the rest of this series spends so much time on tokens and permissions: they are the part of the path you actually govern.
Now picture the web team's portal. Its server code calls https://<YOUR_MY_DOMAIN>.my.salesforce.com/services/data/v67.0/... with an Authorization: Bearer header. Different door, different host, different scope. The layers are the same.
Layer by layer
Each layer answers one question and has an owner. This is the table I would put on Arjun's whiteboard.
| Layer | Question it answers | What lives there | Who owns it | Covered on |
|---|---|---|---|---|
| Client | Where does the person work? | Claude, ChatGPT, Cursor, your web or mobile app, an automation | The client's owner (you, for your own app) | Days 6, 10, 15 |
| Identity | Who is asking? | External Client Apps, OAuth 2.0 with PKCE, named users | Salesforce admin | Days 5, 11 |
| Access | Which door, and what can it do? | Hosted MCP servers; REST, GraphQL and UI API; Salesforce CLI | Salesforce admin and developers | Days 4, 7, 12, 13 |
| Platform | What does the work, under which rules? | Data, Apex, Flow, sharing, validation | Salesforce admin and developers | Days 8, 9, 12 |
| Observability | What happened, and was it allowed? | API logs, event logs, your app's own logs | Admin plus the client's owner | Days 11, 14 |
Clients
Salesforce's developer blog separates "Two kinds of MCP tools": tools for coding agents, where the consumer is a developer building software, and tools for business agents, where the consumer is "an agent serving an end user". Neha's assistant is the second kind. For hosted MCP, Salesforce has tested Claude, ChatGPT, Cursor, Postman and Agentforce Vibes, and says "Other clients that support OAuth 2.0 Authorization Code with PKCE should also work." Your own web app is a client too, whether it calls the REST API directly or, as on Days 10 and 15, calls Claude's API and lets Claude use the hosted servers.
Identity
This is the layer where older tutorials go wrong. An External Client App is how every client identifies itself to your org: "packageable frameworks to enable a third-party application to integrate with Salesforce using APIs and security protocols." For hosted MCP it is the only option: "Connected Apps aren't supported." Since Spring '26 the ability to create new connected apps is disabled by default in every org anyway.
Salesforce recommends "a dedicated ECA per MCP client (one for Claude, one for ChatGPT, one for Cursor, etc.)". In Acme's case that means one app for Claude and one for the portal. Two apps give you two sets of policies, two token lists to revoke from, and two clean lines in your logs.
The same direction applies outside MCP. Salesforce's Summer '26 guide says SOAP login() in API versions 31.0 to 64.0 retires in Summer '27 and tells teams to "Move those integrations to OAuth — using external client apps with JWT tokens". If Arjun's org still has a password-based integration somewhere, this architecture review is a good time to find it.
Access
There are two families of doors:
- Hosted MCP servers. Salesforce-managed endpoints that expose tools. The standard servers, such as
sobject-all(11 tools) and the read-onlysobject-reads(6 tools), have fixed tool sets. Custom servers add your own Apex and Flow as tools. The Headless 360 MCP Server is a beta. All of them live underhttps://api.salesforce.com/platform/mcp/v1/. - Platform APIs. REST API "provides you with programmatic access to your data in Salesforce" at
https://MyDomainName.my.salesforce.com/services/data/vXX.X/resource/. GraphQL offers "a single endpoint to call for all data needed in one request". UI API is "the same API that Salesforce uses to build Lightning Experience" and it "Checks field-level security settings, sharing settings, and perms." Summer '26 ships API version 67.0.
The Salesforce CLI, with its 220+ commands, is a third door, used mostly by coding agents. We come back to it on Day 12.
Platform
Nothing new lives here, and that is the point. Your objects, sharing model, validation rules, Apex and Flows are the same ones Lightning uses. When a custom tool calls invocable Apex, Salesforce says "Apex Actions run as the authenticated user — governor limits and sharing rules apply". An autolaunched Flow exposed as a tool "runs as the authenticated user, not as a system context." Business logic you already trust is business logic every door can reuse, which is why Day 12 builds tools from Apex and Flow rather than from new code in the client.
Observability
For MCP traffic, Salesforce says "agent actions appear in standard Salesforce API logs with full user attribution", and you can find it by filtering for API_CLIENT_CATEGORY = SALESFORCE_HOSTED_MCP. The quick daily check is Setup → Company Information → API Requests, Last 24 Hours. But Salesforce's Help page on headless governance warns that "Salesforce monitoring stops at the Salesforce boundary", so your client has to log its own side of the story: which prompt led to which tool call. Day 14 builds that.
Two ways in: MCP or direct APIs
Here is the decision most teams face first. Both doors reach the same data under the same rules, so the choice is about who decides what to call, and when.
| Hosted MCP servers | REST, GraphQL, UI API | |
|---|---|---|
| Who picks the operation | The model, at run time, from tool names and descriptions | Your code, at build time |
| Host | api.salesforce.com |
Your org's My Domain |
| OAuth scope | mcp_api (+ refresh_token): "access to MCP, but not our existing REST APIs" |
api: "full access to the Platform APIs (REST, Tooling, Metadata, etc.)" |
| Sign-in | Authorization Code with PKCE, always a named user | An External Client App and an OAuth 2.0 flow you choose |
| Shape of the work | A fixed tool set per standard server, plus custom tools for your logic | Any resource the API exposes |
| Result size | soqlQuery: "Maximum 50,000 total records per transaction"; find: "a maximum of 2,000 records" |
Your pagination code decides |
| API quota | Each tool invocation "counts as one or more API calls" | Each request counts |
| Best for | Open-ended questions from people, in AI clients | Known screens and fixed workflows in apps you build |
The scope row is the one that surprises people. Salesforce created mcp_api "to avoid exposing the "Manage user data via APIs (api)" scope that grants full access to the Platform APIs". A token Claude holds for your hosted servers can't call your REST API, by design. That is a feature: a leaked MCP token is limited to what the MCP tools do.
My rule of thumb:
- A person asks questions you can't predict? Use MCP. The model chooses the tool, and the user's permissions bound what it can see.
- The screen is known in advance? Call the API directly. The portal page that lists Acme's open cases doesn't need a model to decide which query to run.
- You need both? Build the app that owns the interface and let it call a model that uses MCP. That is Days 10 and 15. One caution: no Salesforce or Anthropic document yet shows Claude's API connector working with Salesforce's hosted servers end to end, so we treat that pairing as an experiment and test it before we rely on it.
What runs as whom
This section is a preview of Day 11, because it decides Arjun's third proposal.
For hosted MCP, Salesforce's security guide is explicit: "Every MCP tool call runs with the same permissions as the user who authorized the connection". The agent gets the user's object permissions, respects field-level security, follows sharing rules, and "All actions are attributed to the named user in audit trails." The wiki puts it in one line: "If the user can't do it in the Lightning UI or via the REST API, they can't do it via MCP."
The same guide closes the door on the shared integration user: "The system uses OAuth authorization code flow exclusively, maintaining human accountability for all transactions. There are no service accounts, no machine-to-machine flows, and no autonomous operation outside of user context." Salesforce's June security blog calls a shared principal user "an anti-pattern that customers should avoid."
So, for Acme:
| Caller | Door | Runs as | What bounds it |
|---|---|---|---|
| Neha, through Claude | Hosted MCP | Neha | Her profile, permission sets, sharing, the server's tool set |
| The customer portal | REST or GraphQL | The user the portal signs in with | That user's permissions and the app's own checks |
| A nightly job with no person present | Not hosted MCP: there is no machine-to-machine flow | Its own user, through its own External Client App | That user's permissions; design it on Day 11 |
| Arjun testing in Setup | Hosted MCP | Arjun, a System Administrator | Almost nothing, which is why he tests with a normal user too |
Two more facts shape the design. First, you pick which servers exist in your org, but "You cannot restrict access to a specific MCP server through the ECA configuration, but you control access to the tools that compose those servers." Server choice helps; permission sets are the real boundary. Second, this model is about to gain a new piece. A Salesforce knowledge article from September 17, 2026 describes agent registration, which "carves out a discrete identity for each agent instead of letting it operate under the identity of the person it assists", with "Targeting November" as the timing. Until that ships and is documented, design for what exists today: every hosted MCP call is a person.
Don't design around a shared user
If a proposal starts with "one integration user for all AI access", stop there. Hosted MCP doesn't support it, and for direct API integrations it hides who did what. Give each person their own sign-in, and give each client its own External Client App.
Hands-on: map your org and read its limits
Two parts today. The first needs only a pen. The second needs your Developer Edition org and the Salesforce CLI.
Step 1: fill in your target architecture
Copy this table and fill in the last column for your own org. The Acme column is the answer this series builds towards.
| Layer | Question | Acme Global Tech | Your org |
|---|---|---|---|
| Clients | Who asks, and from where? | Neha in Claude; a Next.js assistant on Day 15 | |
| Identity | Which External Client Apps, one per client? | Claude MCP Test and Headless Assistant Local (both Day 5) |
|
| Access | Which servers or APIs, read or write? | sobject-reads first, then sobject-all with confirmation, plus one custom server |
|
| Platform | Which permission set bounds the assistant? | Headless_Assistant_User (Day 11) |
|
| Observability | Where will you look when something goes wrong? | API Requests, Last 24 Hours; event logs; the app's tool trace | |
| Limits | What is your daily API allocation? | Read it in Step 3 |
If a cell stays empty, that is your to-do list for the next two weeks.
Step 2: get a token for the REST API
The REST API needs a Bearer token for a user in your org, and it must be a token that is allowed to call the REST API. An mcp_api token, like the one Claude holds from Day 6, won't work here, by design. The simplest source today is the Salesforce CLI, which holds a token for the org you logged in to:
sf org login web --alias headless360 --set-default
sf org display --target-org headless360
The sf org display output includes your access token and instance URL. Treat the token like a password: don't paste it into chats, tickets or screenshots. In the terminal you will run the script from, set the instance URL, then read the token with read -rs so it isn't echoed, kept in your shell history or written to a file:
export SF_INSTANCE_URL="https://<YOUR_MY_DOMAIN>.my.salesforce.com"
read -rs SF_ACCESS_TOKEN && export SF_ACCESS_TOKEN # paste the token, then press Enter
Step 3: read the API version and daily limits
The script makes two calls. The first asks the org which API versions it serves (/services/data/). The second reads the limits resource for the newest one (/services/data/vXX.X/limits/), which lists "the maximum allocation and the remaining allocation" for each limit in your org.
// check-org-api.ts: read your org's API versions and daily API allocation over REST.
// Run with Node.js 22.19+: npx tsx check-org-api.ts
const INSTANCE_URL = process.env.SF_INSTANCE_URL ?? "https://<YOUR_MY_DOMAIN>.my.salesforce.com";
const ACCESS_TOKEN = process.env.SF_ACCESS_TOKEN; // a REST-capable token, never the mcp_api one
type ApiVersion = { label: string; url: string; version: string };
type Limit = { Max: number; Remaining: number };
async function getJson<T>(path: string): Promise<T> {
const response = await fetch(`${INSTANCE_URL}${path}`, {
headers: ACCESS_TOKEN ? { Authorization: `Bearer ${ACCESS_TOKEN}` } : {},
});
if (!response.ok) {
throw new Error(`${path} failed with HTTP ${response.status}: ${await response.text()}`);
}
return (await response.json()) as T;
}
async function main(): Promise<void> {
// 1. The versions list: which API versions this org serves.
const versions = await getJson<ApiVersion[]>("/services/data/");
const latest = versions.reduce((a, b) => (Number(b.version) > Number(a.version) ? b : a)); // highest version, whatever the order
console.log(`Latest API version: ${latest.version} (${latest.label})`);
// 2. The limits resource for that version (needs the Bearer token).
if (!ACCESS_TOKEN) throw new Error("Set SF_ACCESS_TOKEN to read limits.");
const limits = await getJson<Record<string, Limit>>(`${latest.url}/limits/`);
const api = limits.DailyApiRequests;
console.log(`Daily API requests: ${api.Max - api.Remaining} used of ${api.Max}`);
}
main().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : error);
process.exit(1);
});
Python version
"""check_org_api.py: read your org's API versions and daily API allocation over REST (Python 3.10+)."""
import json
import os
import sys
import urllib.request
from typing import Any
from urllib.error import HTTPError
INSTANCE_URL = os.environ.get("SF_INSTANCE_URL", "https://<YOUR_MY_DOMAIN>.my.salesforce.com")
ACCESS_TOKEN = os.environ.get("SF_ACCESS_TOKEN") # a REST-capable token, never the mcp_api one
def get_json(path: str) -> Any:
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"} if ACCESS_TOKEN else {}
request = urllib.request.Request(f"{INSTANCE_URL}{path}", headers=headers)
try:
with urllib.request.urlopen(request, timeout=20) as response:
return json.load(response)
except HTTPError as error:
sys.exit(f"{path} failed with HTTP {error.code}: {error.read().decode()}")
def main() -> None:
versions = get_json("/services/data/")
latest = max(versions, key=lambda v: float(v["version"])) # highest version, whatever the order
print(f"Latest API version: {latest['version']} ({latest['label']})")
if not ACCESS_TOKEN:
sys.exit("Set SF_ACCESS_TOKEN to read limits.")
limits = get_json(f"{latest['url']}/limits/")
api = limits["DailyApiRequests"]
print(f"Daily API requests: {api['Max'] - api['Remaining']} used of {api['Max']}")
if __name__ == "__main__":
main()
When it works, the script prints two lines: the newest API version your org serves with its release label, and how many of today's API requests are already used out of the daily maximum. Write the maximum into the Limits row of your table.
Step 4: compare it with Setup
Open Setup → Company Information and find API Requests, Last 24 Hours. It should tell the same story as the script. Keep this page in mind: Salesforce's wiki says "MCP tool calls consume API calls against your org's daily API quota", and this is where you watch that happen once Claude is connected on Day 6.
Why the script asks for the version
You could hard-code v67.0, the Summer '26 version, or use the documented latest alias. Reading the list teaches you what your org serves, and it prints the release label next to the number, which makes support conversations easier.
What can go wrong
These are the architecture mistakes I would look for in any Headless 360 design review.
| Symptom | Cause | Fix |
|---|---|---|
| Every new client needs its own Apex endpoint | Business logic lives in UI-specific controllers | Move rules into invocable Apex or autolaunched Flows that any door can reuse as tools (Day 12) |
| "The assistant can see everything" | One shared, over-privileged integration user | Hosted MCP has no service accounts by design. Give each person their own sign-in, each client its own External Client App, and a least-privilege permission set |
| The integration stops working late in the day | Tool calls and API requests draw on the same daily quota, and nobody watches it | Check API Requests, Last 24 Hours; prefer sobject-reads and filtered queries with a LIMIT for read jobs |
| A REST call fails with the token Claude uses | The mcp_api scope doesn't grant the REST API |
Use a token with the api scope for REST, and keep mcp_api for MCP only |
| The limits call is refused for some users | The limits resource is available "for API users with the View Setup and Configuration permission" | Run it as an admin, or grant that permission deliberately rather than to every user |
| Nobody can say which prompt caused a change | Salesforce's logs end at the Salesforce boundary | Log a correlation ID and every tool call in your client (Day 14) |
A note on production
Architecture diagrams tend to show one happy path. In production, count your doors. Each External Client App, each active hosted server and each integration user is something to review, monitor and eventually retire. Fewer, well-named doors with the smallest useful permissions beat a clever design that nobody can audit.
Today's checklist
- I can draw the five layers of a Headless 360 request: client, identity, access, platform, response.
- I know that hosted MCP servers live on
api.salesforce.comand REST lives on my My Domain. - I can explain why an
mcp_apitoken can't call the REST API. - I know every hosted MCP call runs as a named user, with no service accounts.
- I filled in the target architecture table for my org.
- I ran the script and wrote down my org's daily API allocation.
- I found API Requests, Last 24 Hours in Setup.
Frequently asked questions
Is Headless 360 something I deploy?
No. Hosted MCP servers are Salesforce-managed endpoints, and the APIs already exist in your org. You switch servers on in Setup, create an External Client App, and point clients at URLs. The rename to AIforce didn't change that.
Should my app use MCP or the REST API?
Use MCP when a model decides what to do based on a person's question. Use the REST, GraphQL or UI API when your code already knows which call to make. Many real apps do both: the app owns the interface and a model uses MCP for open-ended questions.
Do MCP tool calls count against my API limits?
Yes. Salesforce's wiki says MCP tool calls consume API calls against your org's daily quota, and each tool invocation counts as one or more API calls depending on the tool. Watch Setup → Company Information → API Requests, Last 24 Hours.
Can I connect Claude through one integration user?
Not to the hosted MCP servers. Salesforce uses the OAuth authorization code flow exclusively, with no service accounts and no machine-to-machine flows. A knowledge article describes agent registration with its own identity, targeted for November 2026; treat it as announced, not available.
Why doesn't the MCP server URL use my My Domain?
Standard hosted servers live on api.salesforce.com, and the token you present ties the request to your org and user. Salesforce also documents a My Domain form of the URL with /d/<your domain>/ in the path, which Day 4 covers.
Does Salesforce run an AI model when Claude calls a hosted server?
No. Salesforce says the processing of a standard hosted MCP server is completely deterministic and the LLM is entirely on the client side. Claude does the reasoning; Salesforce runs the tool as the user.
What's next
We have been saying "MCP" for two days. Tomorrow, on Day 3, we open it up: hosts, clients and servers, tools, resources and prompts, and how a request works in the current specification, revision 2026-07-28, which removed the old handshake. Then you build a small MCP server of your own on your laptop and watch every message it sends.
Sources
Verified against the sources below on September 30, 2026. Salesforce ships Headless 360 changes often: check the linked docs if a screen looks different.
- What Salesforce Headless 360 Means For Developers — Salesforce Developers blog, May 21, 2026
- Security Best Practices (Salesforce Hosted MCP Servers)
- How to Secure Salesforce Hosted MCP Servers — Salesforce Developers blog, June 30, 2026
- REST Resources and Requests (REST API Developer Guide)
- Limits resource (REST API Developer Guide)
- Known Limitations (forcedotcom/mcp-hosted wiki) — API quota for MCP tool calls
- Understand AIforce Impact — Salesforce knowledge article, September 17, 2026
Everything from today on one page. Tap to zoom, or download it for later.
All 15 days in this series
- Day 01What Is Salesforce Headless 360 and Why Does It Matter?
- Day 02Headless 360 Architecture Explained
- Day 03MCP Fundamentals: Client, Server, Tools & Resources
- Day 04Enable & Configure the Salesforce MCP Server




Comments
Loading comments...