Conversations

One thread per contact and channel for WhatsApp, email, web chat and social messages, with the agent's analysis attached.
View as Markdown

A conversation is a text thread between one of your contacts and your workspace on one channel: WhatsApp, email, web chat, Instagram or Messenger. Every message in it, from the contact, from the agent or from a person on your team, is stored as a message on that thread. When the agent that answered the thread finishes, Jelliu attaches its analysis to the conversation: a summary, an outcome, sentiment scores and the fields the agent was configured to collect.

With the Conversations API you can read your inbox, open a thread with its messages, start a new WhatsApp or email conversation, reply as a person, and mark threads where the agent asked for human help as handled.

Phone calls are not conversations. Voice calls have their own record, transcript and analysis under Calls.

How it works

  • One thread per contact and channel. Inbound messages and your own sends are matched to the same conversation by a channel-specific key, so a reply from the contact lands in the thread you started instead of opening a second one. A closed thread is reopened when a new message arrives on it, rather than duplicated.
  • Two statuses. A conversation is active or closed. There is no separate “handed off” status: when the agent needs a person it raises a flag on the thread (see Human handoff) and keeps answering.
  • Analysis arrives later. The analysis is produced after the exchange ends. A background sweep runs every 90 seconds, picks up threads that have been quiet for at least 3 minutes, and stores the result. Until then every analysis field is null (or {} for the two result objects). Each new agent turn resets analysis_fetched_at, and the analysis is fetched again, covering the whole thread, once it goes quiet.

Object

Conversation

Returned by GET /api/conversations (one per item) and GET /api/conversations/{conversationId}. Both endpoints shape the conversation the same way: the stored columns plus a few fields joined from the contact, the agent and the latest message.

FieldTypeNullableDescription
idstring (uuid)NoConversation ID.
tenant_idstring (uuid)NoYour workspace ID.
agent_idstring (uuid)YesAgent bound to the thread. null on a conversation you started through the API until an agent handles it.
contact_idstring (uuid)YesContact on the other side. null only on anonymous or legacy threads.
channelstringNowhatsapp, email, webchat, instagram or messenger.
external_idstringYesInternal key used to match inbound messages to this thread. Treat it as opaque.
statusstringNoactive or closed.
metadataobjectYesFree-form data recorded with the thread.
created_atstring (date-time)NoWhen the thread was created.
updated_atstring (date-time)NoLast activity on the thread, such as a new agent turn, a reopen or close, or a handoff flag and its resolution. The list is ordered by this field.
closed_atstring (date-time)YesWhen the thread was closed. null while active.
last_inbound_atstring (date-time)YesWhen the contact last wrote. null if nothing has arrived from them.
needs_human_atstring (date-time)YesWhen the agent asked for a person. null if it never did.
needs_human_reasonstringYesWhat the agent could not do, written for your team (up to 400 characters). Never shown to the contact.
needs_human_resolved_atstring (date-time)YesWhen a person marked the request handled. null while it is still waiting.
elevenlabs_conversation_idstringYesID of the latest agent session that answered the thread.
summarystringYesTwo or three sentences written by the model that handled the thread. null until analysed.
outcomestringYessuccess, failure or unknown. null until analysed. This vocabulary differs from a call’s outcome.
sentiment_scorenumberYes-1 to 1. null means not analysed, not neutral.
frustration_scorenumberYes0 to 1: how frustrated the contact got across the thread. null when not measured.
sentiment_lownumberYes-1 to 1: the most negative single contact turn. null when not measured.
evaluation_criteria_resultsobjectNoPer-criterion verdicts, keyed by criterion ID, each with result and rationale. {} until analysed.
data_collection_resultsobjectNoFields the agent was configured to extract, keyed by field ID, each with value and rationale. {} until analysed.
analysis_fetched_atstring (date-time)YesWhen the analysis was stored. null means not analysed yet.
analysis_attemptsintegerNoFailed or not-ready fetch attempts. The sweep gives up after 5.
cost_creditsintegerYesProvider credits the thread consumed. null when not reported.
cost_usdstring (decimal)YesCost in USD, serialized as a decimal string. null when not reported.
termination_reasonstringYesHow the last agent session ended.
main_languagestringYesDetected language code, lowercase.
call_success_scorenumberYes0 to 100: the provider’s confidence that the thread met its goal.
tool_call_countintegerYesTool calls the agent made. null means never measured; 0 means measured with no tools used.
mcp_call_countintegerYesMCP tool calls. Same null rule.
rag_lookup_countintegerYesKnowledge-base lookups. Same null rule.
tool_result_bytesstring (integer)YesTotal characters returned by tools, serialized as a string. Same null rule.
contact_namestringYesThe contact’s name.
contact_phonestringYesThe contact’s phone number. For email and web chat contacts without a real number, the contact’s email instead.
contact_statusstringYesThe contact’s status, for example converted or dnc. null on anonymous threads.
agent_namestringYesName of the bound agent.
last_messagestringYesPreview of the latest message, up to 160 characters.
last_message_atstring (date-time)YesWhen the latest message was stored.
last_message_rolestringYesuser or agent. user means the contact wrote last and nobody has answered.
qualificationobjectYesLead score derived from data_collection_results and outcome, the same scoring calls use. See below.

qualification has this shape:

FieldTypeDescription
scoreinteger or null0 to 100, or null when there was nothing to score. Never a default.
intereststring or nullhigh, medium or low.
answeredboolean or nullWhether a real person engaged.
next_stepstring or nullThe concrete commitment reached, if any.
objectionsstring[]Objections recorded by the agent.
reasonsstring[]Which signals produced the score, in product language.

Message

Returned in the messages array of GET /api/conversations/{conversationId}.

FieldTypeNullableDescription
idstring (uuid)NoMessage ID.
conversation_idstring (uuid)NoParent conversation.
tenant_idstring (uuid)NoYour workspace ID.
rolestringNouser for the contact, agent for everything sent from your side: the AI, a person on your team, or a recorded outbound email.
content_typestringNotext by default; image or file when the contact sent an attachment.
contentstringNoMessage text. A template send is recorded as [template] <templateId>.
audio_urlstringYesAudio URL for voice-note messages.
metadataobjectYesProvenance data. See the keys below.
created_atstring (date-time)NoWhen the message was stored.
attachmentobjectYes{ filename, contentType, bytes } when the contact attached a file; otherwise null. Only the description is kept, never the file itself.

Keys you may find in metadata:

KeyMeaning
sourceWho wrote an agent row: ai_reply (generated by the agent), canned_reply (a fixed notice, for example when the agent is paused), reply_override, or outbound_email (an email sent through POST /api/email/send, recorded in the contact’s email thread).
authorOn outbound_email rows: human or agent_tool.
cannedWhich fixed notice a canned_reply carries: budget, paused, suppressed or unattended.
manual, outboundtrue on messages a person sent through the API or the dashboard.
provider_message_idThe messaging provider’s ID for a WhatsApp send.
template_idThe WhatsApp template that was sent.
subjectSubject line of an email you started.
tokens_usedTokens the AI reply consumed.

Treat metadata as informational: read the keys you need and ignore the rest.

Human handoff

An agent working a text channel can decide that a person has to step in: a refund it is not allowed to approve, a request for a manager, a question about something it cannot see. When that happens:

  1. The conversation gets needs_human_at and needs_human_reason, and your workspace receives an in-app notification.
  2. The contact is told nothing. The thread is not closed or paused, and the agent keeps answering normally.
  3. Raising the flag again while it is still waiting does nothing, so needs_human_at keeps meaning “waiting since”.
  4. A person reads the thread, optionally replies with POST /api/conversations/{conversationId}/messages, and marks it handled with POST /api/conversations/{conversationId}/needs-human/resolve. Resolving sends nothing to the contact.

The flag is closed, not erased: needs_human_at and needs_human_reason remain and needs_human_resolved_at is set. If the agent raises it again later, a new episode starts.

Every list response carries needsHumanWaiting, the number of threads waiting across the whole workspace, independent of the page you asked for.

Poll GET /api/conversations?needsHuman=waiting to build a work queue. It returns only unresolved requests, oldest wait first, so the person who has waited longest is at the top.

Common tasks

Read the inbox

1

List active conversations

GET /api/conversations returns active conversations only, most recently active first.

page
integerDefaults to 1

1-based page number.

limit
integerDefaults to 50

Items per page, 1 to 100. Larger values are capped at 100.

needsHuman
string

Pass waiting to return only threads whose handoff request is unresolved, ordered by needs_human_at ascending. Any other value is ignored.

curl -sS "https://api.jelliu.co/api/conversations?page=1&limit=50" \
-H "Authorization: Bearer $JELLIU_API_KEY"
200 OK
{
"data": {
"conversations": [
{
"id": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36",
"tenant_id": "b2e4c6a8-1d3f-4a5b-8c7d-9e0f1a2b3c4d",
"agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"contact_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"channel": "whatsapp",
"external_id": "wa_0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90_+573001234567",
"status": "active",
"metadata": null,
"created_at": "2026-09-14T13:02:11.482Z",
"updated_at": "2026-09-14T15:40:52.117Z",
"closed_at": null,
"last_inbound_at": "2026-09-14T15:40:31.905Z",
"needs_human_at": null,
"needs_human_reason": null,
"needs_human_resolved_at": null,
"elevenlabs_conversation_id": "conv_4201k52xq7d9f0a8b3c6e5g1h2j4",
"summary": null,
"outcome": null,
"sentiment_score": null,
"frustration_score": null,
"sentiment_low": null,
"evaluation_criteria_results": {},
"data_collection_results": {},
"analysis_fetched_at": null,
"analysis_attempts": 0,
"cost_credits": null,
"cost_usd": null,
"termination_reason": null,
"main_language": null,
"call_success_score": null,
"tool_call_count": null,
"mcp_call_count": null,
"rag_lookup_count": null,
"tool_result_bytes": null,
"contact_name": "Ana Gómez",
"contact_status": "pending",
"agent_name": "Asesora comercial",
"last_message_at": "2026-09-14T15:40:52.101Z",
"last_message_role": "agent",
"qualification": {
"score": null,
"interest": null,
"answered": null,
"next_step": null,
"objections": [],
"reasons": ["..."]
},
"contact_phone": "+573001234567",
"last_message": "Claro, te comparto los horarios disponibles para esta semana."
}
],
"total": 128,
"page": 1,
"limit": 50,
"needsHumanWaiting": 2
}
}

total counts active conversations matching the filter, so you can compute the number of pages.

2

Open a conversation and its messages

GET /api/conversations/{conversationId} returns the conversation, in any status, with its messages in chronological order.

curl -sS "https://api.jelliu.co/api/conversations/3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36" \
-H "Authorization: Bearer $JELLIU_API_KEY"
200 OK (abridged)
{
"data": {
"id": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36",
"channel": "whatsapp",
"status": "active",
"contact_name": "Ana Gómez",
"summary": "La clienta ya compró el plan anual y pidió ayuda para cambiar el método de pago.",
"outcome": "success",
"sentiment_score": 0.4,
"frustration_score": 0.1,
"sentiment_low": -0.2,
"data_collection_results": {
"tipo_visitante": { "value": "cliente", "rationale": "Mencionó su suscripción activa." }
},
"analysis_fetched_at": "2026-09-14T15:47:03.552Z",
"qualification": {
"score": null,
"interest": null,
"answered": null,
"next_step": null,
"objections": [],
"reasons": ["Es un cliente actual, no un lead: no se califica."]
},
"messages": [
{
"id": "c7d8e9f0-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"conversation_id": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36",
"tenant_id": "b2e4c6a8-1d3f-4a5b-8c7d-9e0f1a2b3c4d",
"role": "user",
"content_type": "image",
"content": "Este es el comprobante",
"audio_url": null,
"metadata": {
"attachment": { "filename": "comprobante.jpg", "content_type": "image/jpeg", "bytes": 184233 },
"inbound_message_id": "SM8f2a1b3c4d5e6f7a8b9c0d1e2f3a4b5c"
},
"created_at": "2026-09-14T15:40:31.905Z",
"attachment": { "filename": "comprobante.jpg", "contentType": "image/jpeg", "bytes": 184233 }
},
{
"id": "d8e9f0a1-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
"conversation_id": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36",
"tenant_id": "b2e4c6a8-1d3f-4a5b-8c7d-9e0f1a2b3c4d",
"role": "agent",
"content_type": "text",
"content": "Gracias, Ana. Ya lo recibí y lo reviso enseguida.",
"audio_url": null,
"metadata": { "source": "ai_reply", "tokens_used": 412 },
"created_at": "2026-09-14T15:40:52.101Z",
"attachment": null
}
]
}
}

The detail endpoint returns at most the first 50 messages of a thread, oldest first. On a longer thread the most recent messages are not included; use last_message, last_message_at and last_message_role for the latest activity.

Start a conversation

POST /api/conversations sends a first message to a phone number on WhatsApp or an email address, and creates or reuses the thread for it.

recipient
stringRequired

1 to 254 characters. For whatsapp, a strict E.164 number such as +573001234567. For email, an email address.

channel
stringRequired

whatsapp or email. Web chat, Instagram and Messenger conversations cannot be started from the API.

message
string

1 to 4096 characters. Required unless you send a WhatsApp templateId.

subject
string

1 to 998 characters. Required when channel is email.

templateId
string (uuid)

WhatsApp only: an approved template from your workspace. Use it to reach someone who has not written to you in the last 24 hours.

templateVariables
string[] | object

Values for the template placeholders, as an ordered array or an object of string values.

What happens:

  1. Jelliu looks for an existing contact in your workspace: by WhatsApp number for whatsapp, by email (case-insensitive) for email. A contact that already belongs to a campaign is reused and not moved.
  2. If none exists, a contact is created in a workspace campaign named Manual Conversations, created on first use and bound to your oldest agent. A workspace with no agents gets 422.
  3. The message is sent through the channel, with all of its checks: opt-out, suppression list, the WhatsApp 24-hour window and allowed hours, and the daily send cap.
  4. Only after a successful send is the thread created (or reopened) and the message stored with role: "agent".
curl -sS -X POST "https://api.jelliu.co/api/conversations" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"channel": "whatsapp",
"recipient": "+573001234567",
"templateId": "5e8f1a2b-3c4d-4e5f-8a9b-0c1d2e3f4a5b",
"templateVariables": ["Ana", "martes 16 de septiembre"]
}'
201 Created
{
"data": {
"conversationId": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36",
"contactId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"channel": "whatsapp",
"externalMessageId": "SM2d4f6a8c0e1b3d5f7a9c1e3b5d7f9a1c"
}
}

externalMessageId is the provider’s message ID for WhatsApp and null for email.

A freeform WhatsApp message is only accepted when the contact wrote to you within the last 24 hours. Outside that window the request fails with 422 OUTSIDE_24H_WINDOW: send an approved templateId instead.

Reply to a conversation

POST /api/conversations/{conversationId}/messages sends a reply written by a person on your team. The agent is not invoked: only your text is delivered and stored.

message
string

1 to 4096 characters. Required unless you send a templateId.

templateId
string (uuid)

WhatsApp conversations only. Rejected with 400 VALIDATION_FAILED on any other channel.

templateVariables
string[] | object

Values for the template placeholders.

Delivery depends on the thread’s channel:

ChannelDelivery
whatsappSent through your WhatsApp sender, with the same checks as starting a conversation.
emailSent from your connected mailbox. The subject is taken from the thread and prefixed with Re: once; with no subject on record it is Re: su conversación.
webchatNothing to call: the message is stored and pushed to the visitor’s open widget, which also picks it up on its next poll.
instagram, messengerNot available yet: 422 VALIDATION_FAILED.

The provider is called before the message is stored, so a rejected send never leaves a message in the thread that the contact did not receive.

curl -sS -X POST "https://api.jelliu.co/api/conversations/3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36/messages" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Hola Ana, soy Laura del equipo de soporte. Ya actualicé tu método de pago." }'
201 Created
{
"data": {
"messageId": "e9f0a1b2-3c4d-4e5f-8a6b-7c8d9e0f1a2b",
"conversationId": "3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36"
}
}

A reply that fails with 5xx after reaching the provider may still have been delivered. Before retrying, re-read the conversation and check whether your message is there, so the contact is not messaged twice.

Resolve a handoff request

1

Find threads waiting for a person

curl -sS "https://api.jelliu.co/api/conversations?needsHuman=waiting" \
-H "Authorization: Bearer $JELLIU_API_KEY"
2

Mark one as handled

curl -sS -X POST "https://api.jelliu.co/api/conversations/3f6a2c1e-8b4d-4f7a-9e2b-5c8d1a0f7e36/needs-human/resolve" \
-H "Authorization: Bearer $JELLIU_API_KEY"
200 OK
{ "data": { "resolved": true } }

The endpoint answers 404 NOT_FOUND both for a conversation that does not exist and for one that is not waiting (never flagged, or already resolved):

404 Not Found
{
"error": {
"code": "NOT_FOUND",
"message": "Esta conversación no está esperando a nadie."
}
}

Talk to an agent over web chat

POST /api/webchat/message sends a message as a visitor and returns the agent’s reply in the same response. Use it for a custom chat interface on your own backend, or to test an agent. To embed chat on a website, use the web chat widget instead.

message
stringRequired

1 to 5000 characters.

visitorId
string

1 to 100 characters from A-Z, a-z, 0-9, _ and -. Identifies the visitor’s thread. Required unless you pass conversationId.

conversationId
string (uuid)

Continue an existing conversation. Required unless you pass visitorId.

agentId
string (uuid)

The agent that answers. It must belong to your workspace, otherwise 403 FORBIDDEN.

curl -sS -X POST "https://api.jelliu.co/api/webchat/message" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"visitorId": "visitor_8f3a2b1c",
"message": "Hola, ¿qué planes tienen?"
}'
200 OK
{
"data": {
"conversationId": "4a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"reply": "¡Hola! Con gusto te cuento. ¿Para cuántas personas de tu equipo lo necesitas?"
}
}

Two related endpoints work on the same threads: GET /api/webchat/{conversationId}/history returns { id, role, content, contentType, createdAt } for each message (the same first-50 cap applies), and POST /api/webchat/{conversationId}/close closes the conversation and returns { "data": { "closed": true } }.

Agent replies and silence

A few rules decide whether the agent answers at all. They apply to every text channel.

  • Reply budget. The agent sends at most 10 AI replies per conversation in any 24 hours. When the budget is spent, the contact receives one fixed closing notice (stored with metadata.source: "canned_reply" and canned: "budget"), the thread is flagged for a person and closed. Replies from your team, fixed notices and recorded outbound emails do not count toward the budget.
  • Paused or deleted agent. A paused agent answers with a fixed notice and the thread stays open, so it resumes where it left off. A thread whose agent was deleted is answered with a notice and closed; no other agent takes over.
  • Opt-out means total silence. A WhatsApp message that is exactly an opt-out keyword (STOP, STOPALL, STOP ALL, CANCEL, UNSUBSCRIBE, QUIT, END, BAJA, CANCELAR, DARME DE BAJA, DAR DE BAJA, NO MOLESTAR, case-insensitive, trailing punctuation ignored), or a clear request not to be contacted, marks every matching contact as dnc, adds the number to your suppression list and sends nothing back, not even a confirmation. From then on, API sends to that contact are refused.

Errors

Validation failures return 400 VALIDATION_FAILED with a field map in details (see Errors). Refusals from the messaging channel use the standard envelope with the codes below. Note that some of them use 423.

CodeStatusWhen
VALIDATION_FAILED400Invalid body or a conversationId that is not a UUID (Invalid conversationId — must be a UUID); missing subject on email; a recipient that is not E.164 (Phone number must be E.164 format (e.g. +14155551234)) or not an email (Invalid email address); templateId on a non-WhatsApp thread.
VALIDATION_FAILED422Starting a conversation in a workspace with no agents; replying to a thread with no contact; replying on instagram or messenger.
CONVERSATION_NOT_FOUND404The conversation does not exist in your workspace (get and reply).
NOT_FOUND404Resolving a handoff on a conversation that is not waiting, or does not exist.
FORBIDDEN403POST /api/webchat/message with an agentId outside your workspace, or a key without the write scope on a POST.
CONTACT_OPTED_OUT403WhatsApp: the contact opted out or is marked dnc.
NO_WHATSAPP_NUMBER400WhatsApp: the contact has no WhatsApp number.
INVALID_RECIPIENT400WhatsApp: the provider rejected the destination number.
TEMPLATE_NOT_FOUND404WhatsApp: templateId is not a template of your workspace.
OUTSIDE_24H_WINDOW422WhatsApp: freeform text to a contact who has not written in the last 24 hours. Send a template.
TEMPLATE_NOT_APPROVED422WhatsApp: the template is not approved yet.
SENDER_NOT_REGISTERED409 or 422WhatsApp: your sender number is still being approved or is offline (409), or no WhatsApp sender is set up (422).
COMPLIANCE_BLOCKED423WhatsApp: blocked by your compliance rules, such as the suppression list, a blocked prefix or allowed hours.
DAILY_CAP_REACHED429WhatsApp: the workspace’s daily send cap is used up.
RATE_LIMITED429WhatsApp: the messaging provider throttled the send. Retry shortly.
WHATSAPP_SEND_FAILED500 or 502WhatsApp: an unexpected provider failure.
EMAIL_SEND_FAILED400, 403, 409, 423, 429Email, told apart by status: 400 the contact has no email address; 403 the contact opted out; 409 no connected mailbox, or the mailbox connection is not working; 423 blocked by compliance rules; 429 daily email cap reached.
RATE_LIMIT_EXCEEDED429A request rate limit. See Limits.

A provider refusal looks like this:

422 Unprocessable Entity
{
"error": {
"code": "OUTSIDE_24H_WINDOW",
"message": "Outside the 24-hour window: this contact has not messaged you in the last 24h, so Meta requires an approved template to reach them. Pick a template to send."
}
}

Limits

EndpointScopeRate limit
GET /api/conversationsreadGeneral API limit, by plan.
GET /api/conversations/{conversationId}readGeneral API limit.
POST /api/conversationswriteGeneral API limit and the shared 10 per minute budget for mutations.
POST /api/conversations/{conversationId}/messageswriteGeneral API limit.
POST /api/conversations/{conversationId}/needs-human/resolvewriteGeneral API limit.
POST /api/webchat/messagewriteGeneral API limit and 20 per minute per client IP.
GET /api/webchat/{conversationId}/historyreadGeneral API limit.
POST /api/webchat/{conversationId}/closewriteGeneral API limit.

See Rate limits for the per-plan general limit and Authentication for scopes. None of these endpoints requires a full key.

Other limits:

  • Daily send caps. Each workspace can send up to 1,000 WhatsApp messages and 1,000 emails per day by default. A failed send does not consume the cap. Contact support to raise it.
  • Plan allowance. AI replies on text channels, web chat included, count toward your plan’s monthly included chat messages.
  • Reply budget. 10 AI replies per conversation per 24 hours, as described in Agent replies and silence.
  • Freshness. List and detail responses are cached for up to 30 seconds. A reply sent through the API clears the cache for its conversation and your inbox, so an immediate re-read includes it.

Webhooks

There are no dedicated conversation events. Related events from the catalog:

EventRelation to conversations
crm_sync.completed, crm_sync.failedAn interaction was written, or failed to write, to a connected CRM. Carries conversationId and channel.
agent.action_recordedThe agent invoked a tool. Carries conversation_id when it happened in a conversation.

To react to new messages or handoff requests, poll GET /api/conversations (for example with needsHuman=waiting) and compare updated_at and last_message_at.