CRM integration (external_id)

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".
  • The external_id is stored on each message’s metadata (not on the conversation itself) and is returned to you in the response.

  • You cannot set external_id at conversation creation time. The “Create conversation” endpoint only accepts a name. 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?

  • The Conversation object has no external_id field — only IDs like id and session_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:

  1. 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 by name.

  2. Send messages to that conversation and include external_id on every call. The response returns the created message (PromptHistory) including conversation_id and metadata.external_id, which is useful for logging and verification.

  3. 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_id here; only a name is 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)

Where to put it? • Conversations Messages API → external_id query 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)

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_id is a query param; prompt and other fields are sent as multipart form‑data. response_source accepts default | 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)

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 here

Why 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_id on every message. That ensures message‑level attribution is consistent, and every reply carries the CRM identity within metadata.
  • Use one conversation per user (or per case/ticket) and persist { external_id -> session_id } in your system of record. You can read session_id from the create‑conversation response and keep reusing it.
  • Name convention for discovery: Because external_id is not a conversation‑level field, putting crm:{id} in the conversation name makes it searchable through the conversations list filter.
  • Max length: external_id supports up to 128 characters. Trim/normalize or hash long IDs.
  • Streaming: Both the messages endpoint and the OpenAI‑style endpoint support streaming; external_id usage is the same, but you’ll process SSE on the client.
  • Request source analytics: The platform tracks request sources like api and api-openai in traffic analytics; this is separate from your external_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_id query 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_id field. 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_id in message/conversation list endpoints. Retrieve messages and filter client‑side, or rely on your own mapping table.
  • external_id length limit of 128 chars.

Putting it all together (pattern)

  1. On first contact for CRM user U:

    • POST /projects/{projectId}/conversations with name: "crm:U" → store session_id.
    • POST /projects/{projectId}/conversations/{session_id}/messages?external_id=U with the user’s prompt → verify metadata.external_id.
  2. On subsequent turns:

    • Reuse the session_id and always pass external_id=U again with each POST .../messages.

This gives you durable threads plus end‑user attribution in every message — exactly what you need for a clean CRM integration.