Refer this document Enable and use CRM integration for a no-code way of using the external_id for CRM integration.
If you are interfacing with the agent solely through the API:
Below is a developer‑focused guide for identifying end‑users (e.g., CRM contacts) when you use the CustomGPT API only (no web widget), by attaching an external_id to every API message. It includes what’s possible, the recommended patterns, and cut‑and‑paste examples.
TL;DR (what’s possible)
-
Yes — you can label each API message with your own
external_id(e.g., a CRM contact ID).- For the Conversations Messages API, pass it as a query parameter
external_id(max 128 chars). - For the OpenAI‑style Chat Completions API, put it in the JSON body as
"external_id".
- For the Conversations Messages API, pass it as a query parameter
-
The
external_idis stored on each message’s metadata (not on the conversation itself) and is returned to you in the response. -
You cannot set
external_idat conversation creation time. The “Create conversation” endpoint only accepts aname. Consider embedding your external ID in the conversation name as a helper/convention. -
To retrieve it later, list the conversation’s messages; each item contains
metadata.external_id.
Why attach external_id at the message level?
external_id at the message level?- The Conversation object has no
external_idfield — only IDs likeidandsession_id. So the canonical place to record your end‑user identity is message metadata, which the platform already persists and returns.
Recommended architecture (API‑only)
Goal: Route every request under a known conversation (via session_id) and consistently tag each message with external_id.
Flow:
-
Create a conversation (once per end‑user “thread”). Tip: Put the CRM ID in the conversation name (e.g.,
crm:12345 – ACME Inc.) for easy admin search later. The “list conversations” endpoint lets you filter byname. -
Send messages to that conversation and include
external_idon every call. The response returns the created message (PromptHistory) includingconversation_idandmetadata.external_id, which is useful for logging and verification. -
Persist a mapping in your app datastore:
external_id (CRM) -> conversation.session_id
so your backend can quickly route subsequent requests without searching.
Step‑by‑step with examples
1) Create a conversation
You cannot attach
external_idhere; only anameis supported. Use the name convention if helpful.
cURL
curl --request POST \
--url "https://app.customgpt.ai/api/v1/projects/123/conversations" \
--header "Authorization: Bearer $CUSTOMGPT_API_KEY" \
--header "Content-Type: application/json" \
--data '{"name": "crm:EXT_12345 — ACME Inc."}'Response (abridged) – save data.session_id for later:
{
"status": "success",
"data": {
"id": 42,
"name": "crm:EXT_12345 — ACME Inc.",
"session_id": "f1b9aaf0-5e4e-11eb-ae93-0242ac130002"
}
}(Conversation schema includes session_id but no external_id.)
2) Send a message to the conversation (attach external_id)
external_id)Where to put it? • Conversations Messages API →
external_idquery param (max length 128). • OpenAI‑style Chat Completions →"external_id"in JSON body.
Option A — Conversations Messages API (recommended when you need the session_id thread)
session_id thread)cURL
curl --request POST \
--url "https://app.customgpt.ai/api/v1/projects/123/conversations/f1b9aaf0-5e4e-11eb-ae93-0242ac130002/messages?stream=false&lang=en&external_id=EXT_12345" \
--header "Authorization: Bearer $CUSTOMGPT_API_KEY" \
--header "accept: application/json" \
--header "content-type: multipart/form-data" \
--form 'prompt=What are your support hours?' \
--form 'response_source=default'Notes:
external_idis a query param;promptand other fields are sent as multipart form‑data.response_sourceacceptsdefault | own_content | openai_content.
Response (PromptHistory, abridged)
You’ll see your external ID echoed back in data.metadata.external_id, and the conversation_id you can log:
{
"status": "success",
"data": {
"id": 1,
"user_query": "What are your support hours?",
"openai_response": "Our support hours are ...",
"conversation_id": 42,
"citations": [],
"metadata": {
"external_id": "EXT_12345",
"request_source": "api"
}
}
}(PromptHistory includes metadata.external_id, and the value is stored server‑side.)
Option B — OpenAI‑style Chat Completions (when you prefer OpenAI JSON format)
cURL
curl --request POST \
--url "https://app.customgpt.ai/api/v1/projects/123/chat/completions" \
--header "Authorization: Bearer $CUSTOMGPT_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"external_id": "EXT_12345",
"messages": [
{"role": "user", "content": "What are your support hours?"}
],
"lang": "en",
"stream": false
}'Important: This endpoint returns an OpenAI‑style response (choices, usage, etc.) and does not include conversation/session IDs in the response payload. If you need to manage long‑lived conversations (session IDs) in your own app, prefer Option A.
3) Retrieve messages (to audit or verify external_id)
external_id)cURL
curl --request GET \
--url "https://app.customgpt.ai/api/v1/projects/123/conversations/f1b9aaf0-5e4e-11eb-ae93-0242ac130002/messages?order=desc" \
--header "Authorization: Bearer $CUSTOMGPT_API_KEY"Each item in data.messages.data is a PromptHistory object; check metadata.external_id.
Code recipes (server‑side)
Node.js and Python (fetch)
import fetch from "node-fetch";
const API = "https://app.customgpt.ai/api/v1";
const API_KEY = process.env.CUSTOMGPT_API_KEY;
const projectId = 123;
// Resolve or create a conversation for a CRM user
async function ensureConversation(crmId) {
// You probably store this mapping in your DB:
let sessionId = await db.getSessionForExternalId(crmId);
if (sessionId) return sessionId;
// Create conversation (you cannot set external_id here)
const res = await fetch(`${API}/projects/${projectId}/conversations`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ name: `crm:${crmId}` }) // name-only at create
});
const body = await res.json();
sessionId = body?.data?.session_id; // save for subsequent calls
await db.saveMapping(crmId, sessionId);
return sessionId;
}
// Send a message tagged with external_id
async function sendMessage(sessionId, crmId, prompt) {
const url = `${API}/projects/${projectId}/conversations/${sessionId}/messages?stream=false&lang=en&external_id=${encodeURIComponent(crmId)}`;
const form = new FormData();
form.append("prompt", prompt);
form.append("response_source", "default");
const r = await fetch(url, {
method: "POST",
headers: { "Authorization": `Bearer ${API_KEY}` },
body: form
});
const json = await r.json();
return json; // includes metadata.external_id + conversation_id
}import os, requests
API = "https://app.customgpt.ai/api/v1"
API_KEY = os.environ["CUSTOMGPT_API_KEY"]
PROJECT_ID = 123
def create_conversation(crm_id: str) -> str:
# Only 'name' is supported at creation time
url = f"{API}/projects/{PROJECT_ID}/conversations"
r = requests.post(
url,
headers={"Authorization": f"Bearer {API_KEY}"},
json={"name": f"crm:{crm_id}"}
)
r.raise_for_status()
return r.json()["data"]["session_id"]
def send_message(session_id: str, crm_id: str, prompt: str):
url = f"{API}/projects/{PROJECT_ID}/conversations/{session_id}/messages"
params = {"stream": "false", "lang": "en", "external_id": crm_id}
files = {"prompt": (None, prompt), "response_source": (None, "default")}
r = requests.post(url, headers={"Authorization": f"Bearer {API_KEY}"}, params=params, files=files)
r.raise_for_status()
return r.json() # metadata.external_id available hereWhy this works: external_id is accepted on the messages endpoint as a query param and is stored in message metadata.
Design tips & operational guidance
- Always include
external_idon every message. That ensures message‑level attribution is consistent, and every reply carries the CRM identity withinmetadata. - Use one conversation per user (or per case/ticket) and persist
{ external_id -> session_id }in your system of record. You can readsession_idfrom the create‑conversation response and keep reusing it. - Name convention for discovery: Because
external_idis not a conversation‑level field, puttingcrm:{id}in the conversation name makes it searchable through the conversations list filter. - Max length:
external_idsupports up to 128 characters. Trim/normalize or hash long IDs. - Streaming: Both the messages endpoint and the OpenAI‑style endpoint support streaming;
external_idusage is the same, but you’ll process SSE on the client. - Request source analytics: The platform tracks request sources like
apiandapi-openaiin traffic analytics; this is separate from yourexternal_id.
FAQ
Q: Can I set external_id when creating the conversation?
A: Not directly. The create endpoint only supports name. Set external_id when you send messages, and (optionally) include it in the conversation name for discovery.
Q: How do I verify that my external_id “stuck”?
A: Inspect the send‑message response (PromptHistory.metadata.external_id) or list the conversation messages and check each item.
Q: If I use the OpenAI‑style endpoint, how do I keep a persistent thread?
A: That endpoint doesn’t expose a session_id in the response (it returns standard OpenAI fields). If you need to manage threads explicitly, prefer the Conversations endpoints (create → send messages).
Full endpoint references cited
- Send message to a conversation (supports
external_idquery param; multipart form) - OpenAI‑style chat completions (supports
"external_id"in JSON body) - PromptHistory schema (shows
metadata.external_id) - Create conversation (name‑only; no
external_id) - List conversation messages (returns PromptHistory items with metadata)
- Conversations listing supports name filter (useful if you embed CRM ID in name)
- Traffic analytics request sources (
api,api-openai, etc.)
Limitations to be aware of
- No conversation‑level
external_idfield. You must carry it on each message; maintain your own{external_id ↔ session_id}map if you need fast lookups. - No server‑side filtering by
external_idin message/conversation list endpoints. Retrieve messages and filter client‑side, or rely on your own mapping table. external_idlength limit of 128 chars.
Putting it all together (pattern)
-
On first contact for CRM user U:
POST /projects/{projectId}/conversationswithname: "crm:U"→ storesession_id.POST /projects/{projectId}/conversations/{session_id}/messages?external_id=Uwith the user’s prompt → verifymetadata.external_id.
-
On subsequent turns:
- Reuse the
session_idand always passexternal_id=Uagain with eachPOST .../messages.
- Reuse the
This gives you durable threads plus end‑user attribution in every message — exactly what you need for a clean CRM integration.
