Agentforce | Agent API로 Salesforce 외부에서 에이전트와 메시지 주고받는법
안녕하세요, 루카입니다. 오늘은 External Client App을 만들고 curl로 Agent API를 호출해, Salesforce 밖에서 Agentforce 에이전트와 메시지를 주고받아 보겠습니다.
Agent API는 Salesforce 밖의 프로그램이 Agentforce 에이전트에게 메시지를 보내고 답을 받게 해 주는 기능입니다.
홈페이지나 사내 시스템이 자기 화면에 에이전트의 답을 보여 주려면, 그 시스템이 이 API를 호출해야 합니다.
준비할 것
- org와 권한: Agentforce가 켜진 org와 시스템 관리자 권한이 필요합니다.
- 터미널: curl을 쓸 수 있는 bash 계열 터미널(macOS, Linux, Windows는 Git Bash)을 준비합니다.
- 사용자 라이선스: 이번에는 연동 사용자에게 Salesforce Integration 라이선스를 썼습니다. 남은 수는 Setup의 Company Information에서 확인합니다.
완료 확인: 메시지를 보내면 에이전트의 답(type Inform)이 오고, 세션을 닫았을 때 SessionEnded가 오면 끝입니다.
에이전트 준비
Agent API로 부를 에이전트를 만들어 활성화한 뒤, Agent ID는 빌더 주소가 아니라 조회로 얻습니다.
1. Service 에이전트를 만듭니다
Setup의 Agentforce Agents에서 New Agent를 누르고, 새 빌더 안내 창의 Create Agent를 누릅니다. 새 탭의 템플릿 목록에서 Agentforce Service Agent의 Select를 누릅니다.
Agent API 문서에 따르면 Agentforce (Default) 유형은 Agent API로 부를 수 없습니다.

Name your agent 창에서 이름과 Developer Name을 넣고, New User를 고른 뒤 Let's Go를 누릅니다. New User를 고르면 이 에이전트가 동작할 때 쓰는 에이전트 사용자(Agent User)가 함께 만들어집니다.

2. 허용 언어에 Korean을 추가합니다
빌더의 Language Settings에서 Allowed Languages에 Korean을 더하고 저장합니다. 이렇게 둔 뒤 에이전트는 한국어 질문에 한국어로 답했고, 첫인사는 기본 언어인 영어로 보냈습니다.
3. 버전을 커밋하고 활성화합니다
빌더에서 Commit Version을 누르고 확인 창의 Commit을 누릅니다. 커밋하기 전의 Draft 버전에는 Activate 단추가 보이지 않습니다.

커밋이 끝나면 Activate를 누르고, Ready to activate your agent? 창에서 다시 Activate를 누릅니다.


4. Agent ID를 조회합니다
새 빌더의 주소에 보이는 ID는 빌더 프로젝트와 버전의 ID(각각 1bY, 1bZ로 시작)라서 Agent ID가 아닙니다. Agent ID는 Developer Console의 Query Editor에서 아래처럼 조회합니다. 결과의 Id가 0Xx로 시작하면 맞게 찾은 것입니다.
SELECT Id FROM BotDefinition
WHERE DeveloperName =
'LUKA_031_Class_Guide'

DeveloperName에는 만든 에이전트의 Developer Name을 넣습니다.
이번에 시험한 Developer Edition org 하나에서는 커밋과 활성화가 계속 실패했고, 내부 응답은 "Changes for this Bot Version are not allowed at the moment"였습니다. 다른 org에서는 같은 순서로 활성화됐습니다.
앱 만들기
External Client App에서 Client Credentials Flow를 켜고, API만 쓸 수 있는 연동 사용자를 Run As로 지정합니다.
External Client App은 외부 시스템이 Salesforce API에 접속할 때 쓰는 앱 설정입니다. Agent API 문서는 Connected App이 아니라 이 앱을 만들라고 안내합니다.
1. 연동 사용자를 만듭니다
Setup의 Users에서 New User를 엽니다. User License는 Salesforce Integration, Profile은 Minimum Access - API Only Integrations를 고릅니다.
이번에는 이 사용자에게 권한 집합을 따로 할당하지 않았고, 이 상태로 토큰 발급과 대화가 모두 됐습니다.

2. External Client App을 만듭니다
Setup의 External Client App Manager에서 New External Client App을 누르고 이름과 연락 이메일을 넣습니다.
Enable OAuth를 체크하고 Callback URL에 값을 하나 넣습니다. api, refresh_token offline_access, chatbot_api, sfap_api 네 스코프를 Selected OAuth Scopes로 옮깁니다. 화면에서는 Manage user data via APIs (api)처럼 설명과 함께 보입니다.

Callback URL은 이 방식에서 쓰지 않지만 필수 칸이라, 이번에는 https://localhost/callback을 넣었습니다.
같은 화면 아래쪽의 체크박스 다섯 개를 다음처럼 맞추고 Create를 누릅니다. Agent API 문서가 안내하는 설정이고, 해제하는 두 항목은 이 앱이 쓰지 않는 다른 흐름(Web Server, Refresh Token)의 설정입니다.
- Enable Client Credentials Flow를 체크합니다. 확인 창이 뜨면 OK를 누릅니다
- Require secret for Web Server Flow가 체크되어 있으면 해제합니다
- Require secret for Refresh Token Flow가 체크되어 있으면 해제합니다
- Require Proof Key for Code Exchange (PKCE) 항목은 체크된 채 바꿀 수 없으면 그대로 둡니다
- Issue JSON Web Token (JWT)-based access tokens for named users를 체크합니다

3. 연동 사용자를 Run As로 지정합니다
만든 앱의 Policies 탭에서 Edit를 누릅니다. OAuth Flows and External Client App Enhancements에서 Enable Client Credentials Flow를 다시 체크합니다.
문서도 앱 설정과 정책 두 곳에서 모두 켜도록 안내하고, Run As는 정책 쪽에만 있습니다.
Run As (Username) 칸에 연동 사용자의 Username을 넣고 저장합니다.

4. Consumer Key와 Secret을 확인합니다
앱의 Settings 탭에서 OAuth Settings를 펼치고 Consumer Key and Secret을 누릅니다. 본인 확인 코드를 넣으면 새 탭에 Key와 Secret이 보입니다.

Consumer Secret은 브라우저 코드나 공개 저장소에 두지 않습니다. 이 값이 있으면 누구나 연동 사용자로 토큰을 받을 수 있습니다.
API 호출
토큰은 My Domain 주소로 받고, 세션과 메시지는 api.salesforce.com으로 보냅니다.

1. My Domain 주소를 확인합니다
Setup의 My Domain에서 Current My Domain URL을 확인합니다. 이 주소는 my.salesforce.com으로 끝납니다. 브라우저 주소창에 보이는 lightning.force.com 주소가 아닙니다.

2. 변수를 정하고 토큰을 받습니다
명령마다 같은 값을 다시 쓰지 않도록, 아래 변수에 자기 org의 값을 넣어 둡니다.
DOMAIN=<이름>.my.salesforce.com
KEY=<Consumer Key>
SECRET=<Consumer Secret>
AGENT_ID=<0Xx로 시작하는 ID>
API=https://api.salesforce.com
API+=/einstein/ai-agent/v1
CT="Content-Type: application/json"
URL="https://$DOMAIN"
URL+="/services/oauth2/token"
curl -s -X POST "$URL" \
-d grant_type=client_credentials \
-d client_id="$KEY" \
-d client_secret="$SECRET"
응답의 access_token 값을 TOKEN에 넣습니다. scope에 sfap_api chatbot_api api가 보이면 앱 설정이 맞은 것입니다. 이번 응답의 expires_in은 7200초(약 2시간)였습니다.
TOKEN=<응답의 access_token>
AUTH="Authorization: Bearer $TOKEN"
3. 세션을 엽니다
세션을 여는 요청 본문은 아래 내용을 start.json으로 저장해 보냅니다. externalSessionKey에는 UUID 생성기로 만든 아무 값이나 넣고, endpoint에는 My Domain 주소를 넣습니다.
{
"externalSessionKey": "<UUID>",
"instanceConfig": {
"endpoint": "https://<My Domain>"
},
"streamingCapabilities": {
"chunkTypes": ["Text"]
},
"bypassUser": true
}
curl -s -X POST \
"$API/agents/$AGENT_ID/sessions" \
-H "$AUTH" -H "$CT" \
--data-binary @start.json
응답의 sessionId를 SID에 넣습니다. 응답에는 에이전트의 첫인사 메시지도 함께 옵니다.
SID=<응답의 sessionId>
4. 메시지를 보냅니다
메시지 요청 본문은 아래 내용을 msg.json으로 저장합니다. sequenceId는 세션 안에서 메시지마다 1씩 올립니다.
{
"message": {
"sequenceId": 1,
"type": "Text",
"text": "주말 반 수강 신청은 어떻게 하나요?"
}
}
curl -s -X POST \
"$API/sessions/$SID/messages" \
-H "$AUTH" -H "$CT" \
-H "Accept: application/json" \
--data-binary @msg.json
이번에는 약 9초 뒤 type이 Inform인 응답으로 한국어 답이 왔습니다. 답은 정보를 찾지 못했다며 상담원 연결을 권했습니다. 답의 내용은 에이전트에 연결한 지식과 액션에 따라 달라지므로, 여기서는 Inform 응답이 온 것으로 연결을 확인합니다.
{
"messages": [{
"type": "Inform",
"isContentSafe": true,
"message": "현재 주말 반 수강
신청 방법에 대한 정보를
불러올 수 없습니다. …
상담원 연결을
원하시나요?",
"citedReferences": []
}]
}
세션 시작 응답의 _links에서 messages는 이 동기 주소이고, 스트리밍 주소는 messagesStream에 따로 있습니다. 스트리밍을 쓰면 Accept를 text/event-stream으로 바꿔 /messages/stream에 보냅니다.
5. 세션을 닫습니다
END="x-session-end-reason"
curl -s -X DELETE \
"$API/sessions/$SID" \
-H "$AUTH" -H "$END: UserRequest"
응답의 type이 SessionEnded이고 reason이 ClientRequest면 세션이 닫힌 것입니다. 요청 헤더의 UserRequest와 응답의 ClientRequest가 달라도 정상입니다.
오류 대처
주소나 Secret을 잘못 넣으면 302, 401, 400이 오므로, 어느 요청에서 어떤 코드가 왔는지로 고칠 곳을 찾습니다.
| 응답 | 원인 | 고칠 곳 |
|---|---|---|
| 토큰 요청의 302 | 토큰 주소가 브라우저 주소 | My Domain 주소로 보냄 |
| 토큰 요청의 400 | Key나 Secret이 틀림 | 앱의 Key와 Secret 다시 복사 |
| 세션 시작의 401 | endpoint가 브라우저 주소 | endpoint를 My Domain으로 |
| 메시지의 400 | 한국어를 --data로 직접 넣음 |
UTF-8 파일과 --data-binary |
| 세션의 423 | 요청을 동시에 둘 보냄 | 응답을 받은 뒤 다음 요청 |
- 토큰 요청의 400: Secret이 틀리면 invalid_client("invalid client credentials")가 옵니다.
- 302: 토큰 요청을 lightning.force.com 주소로 보내면 오류 본문 없이 다른 주소로 옮기라는 302만 옵니다.
- 401: 세션 시작 요청의 endpoint에 브라우저 주소를 넣으면 INVALID_SESSION_ID가 옵니다. 문서는 이 경우를 500 EngineConfigLookupException으로 안내하지만 이번에는 401이었습니다.
- 메시지의 400: Windows Git Bash에서 한국어 본문을
--data '…'로 넣으면 "Invalid UTF-8 middle byte"가 옵니다. - 423과 500: 문서에 따르면 한 세션에 요청이 겹치면 423이 오고, 응답이 120초를 넘기거나 Agent ID가 틀리면 500이 옵니다.
권한 범위
bypassUser 값에 따라 에이전트 사용자나 연동 사용자의 권한으로 동작하므로, 외부에 열기 전에 그 사용자의 권한을 확인합니다.
Agent API 문서에 따르면 bypassUser가 true면 에이전트 사용자로, false면 토큰을 받은 연동 사용자로 동작합니다. 이번에는 두 값 모두 세션이 열렸고, 같은 질문에 한국어로 답했습니다. 에이전트에 앱을 따로 연결하는 단계는 없었습니다.

에이전트 사용자는 빌더의 Agent Access에서 Agent's User Record로 확인합니다. 에이전트에 레코드 조회 액션을 추가하면, 외부 방문자도 이 사용자가 볼 수 있는 데이터를 답으로 받을 수 있습니다.

홈페이지 위젯처럼 누구나 호출하는 곳에 열 때는, 쓰는 bypassUser 값에 해당하는 사용자(true면 에이전트 사용자, false면 연동 사용자)의 권한 집합을 필요한 오브젝트만 읽도록 줄여 둡니다.
위젯처럼 브라우저에서 부르는 구성은 Secret을 서버 함수에 두어야 하므로 별도 글에서 다룰 예정입니다.

참고 문서
'Salesforce > Agentforce' 카테고리의 다른 글
| Agentforce | 블로그에 Agentforce 채팅 위젯 설치하기 (0) | 2026.10.19 |
|---|---|
| Agentforce | Draft with Agentforce로 이메일 제목과 본문 초안 생성 (0) | 2026.10.19 |
| Agentforce | Retriever로 규정 문서를 프롬프트 응답의 근거로 활용하는법 (2) | 2026.10.19 |
| Agentforce | Structured Outputs로 JSON 응답 형식을 지정하고 Flow에서 사용 (0) | 2026.10.16 |
| Agentforce | Flow 병합 필드로 열린 문의 데이터를 프롬프트에 반영하는법 (0) | 2026.10.16 |