Notification event types
Updated
Webhook notification events that the Conversational AI Platform sends for sessions, RTC, and telephony.
Preview documentation
When you enable Agora message notifications, the Agora notification server (NCS) sends event notifications about your Conversational AI Platform sessions to your server in HTTPS POST requests. This page describes the request format and each event type.
To receive notifications, configure a webhook for your project in Agora Console.
Choose event types by goal
The following table helps you find the events for common tasks:
| Goal | Use these event types | What they help you answer |
|---|---|---|
| Confirm that a session started or ended | 1001 Session state changed | Did the agent start? When and why did the session end? |
| Get what was said in a session | 1002 Session transcripts | What did the user and the agent say? |
| Investigate failures | 1003 Session errors, 1001 Session state changed | Which component failed? Did the session end because of it? |
| Measure latency per turn | 1004 Turns finished | How long did each turn take, and in which component? |
| Renew the agent's RTC token | 1101 RTC token expiring | Does the agent need a new token? |
| Track phone call progress | 1201 Telephony state | Was the call answered? How and why did it end? |
For phone calls, the session and the call are reported separately. 1001 Session state changed reports the agent's session, which can be running while the call is still ringing. 1201 Telephony state reports the call itself. A call can also end before the session's post-call results are ready. To follow a phone call, handle both events.
Request header
Each notification request contains the following headers:
| Header | Description |
|---|---|
Content-Type | application/json |
Agora-Signature-V2 | An HMAC/SHA256 signature over the raw request body. Use this signature when you can. |
Agora-Signature | An HMAC/SHA1 signature over the raw request body, for compatible integrations. |
Verify the signature against the unchanged raw request body before you accept the notification.
Request body
The request body is a UTF-8 JSON object with the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
noticeId | String | Yes | The notification ID. It stays the same when NCS retries the same event. Use it to deduplicate notifications. |
productId | Integer | Yes | The product ID. For the Conversational AI Platform, 23. |
eventType | Integer | Yes | The event type. See Event types. |
notifyMs | Integer | Yes | The time NCS sent the notification, as a Unix timestamp in milliseconds. It can change when NCS retries. |
sid | String | Yes | The session ID. It's the same as payload.agent_session_id. |
payload | Object | Yes | The event details. Each event type has its own fields. |
The envelope fields use camelCase. The payload fields use snake_case, like the Session API. In payload, created_at and ended_at are the session's timestamps in Unix seconds, and occurred_at_ms is when the event happened, in Unix milliseconds.
{
"noticeId": "notice-1001-example",
"productId": 23,
"eventType": 1001,
"notifyMs": 1789913402100,
"sid": "b42ac95tp25py49fc37hp94vm67ht52h",
"payload": {
"agent_session_id": "b42ac95tp25py49fc37hp94vm67ht52h",
"status": "failed",
"occurred_at_ms": 1789913402000,
"created_at": 1789913400,
"ended_at": 1789913402,
"transport": {
"type": "telephony",
"direction": "outbound",
"to_number": "+14155550101"
}
}
}Respond to notifications
After you verify a notification, return HTTP status 200 with a JSON body within 10 seconds. Keep the following in mind:
- Notifications can be duplicated or arrive out of order. Neither
notifyMsnoroccurred_at_mssets the delivery order. Deduplicate bynoticeId. - Acknowledge event types that you don't recognize, after you verify their signature.
- If you miss a notification, Retrieve a session returns the session's latest status, not the transition you missed. Phone call state and post-call results can't be recovered this way.
Event types
Event types are grouped by scope: 10xx for sessions, 11xx for RTC, and 12xx for telephony. Session events apply to both transports.
| Event type | Event name | Event description |
|---|---|---|
| 1001 | Session state changed | The session status changes, including when the agent joins or leaves an RTC channel and when the session fails to start. |
| 1002 | Session transcripts | Transcript processing for the session finished or failed. |
| 1003 | Session errors | A component of the agent reported an error. The error doesn't necessarily end the session. |
| 1004 | Turns finished | Conversation turn data, including metrics for each turn, after the session ends. |
| 1101 | RTC token expiring | The agent's RTC token is about to expire. |
| 1201 | Telephony state | The state of an inbound or outbound phone call changes. |
1001 Session state changed
The session status changed. This event applies to both transports, and it includes failures before the agent joins an RTC channel or before a call is answered. For RTC sessions, running means the agent joined the channel, and stopped or failed means it left.
payload.status is the status at the transition. A later Retrieve a session request can return a newer status.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
agent_name | String | No | The agent name, when it's known. |
labels | Object<string, string> | No | The labels you passed when you started the session. |
status | String | Yes | The session status at this transition: idle, starting, running, stopping, stopped, or failed. |
occurred_at_ms | Integer | Yes | When the transition happened, as a Unix timestamp in milliseconds. |
created_at | Integer | Yes | When the session was created, as a Unix timestamp in seconds. |
ended_at | Integer | No | When the session ended, as a Unix timestamp in seconds, if it's known. |
transport | Object | Yes | The session's transport. For rtc, type and channel. For telephony, type, direction, and the from_number and to_number when they're known. It never contains RTC tokens or encryption settings. |
message | String | No | Details about the status, as free-form text, for example OK. Don't rely on a fixed set of values. |
A session can fail before the phone numbers are resolved. In that case, transport doesn't contain from_number or to_number.
The following examples show payloads for RTC and phone sessions:
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"agent_name": "support-agent",
"status": "running",
"occurred_at_ms": 1789913400000,
"created_at": 1789913400,
"transport": {
"type": "rtc",
"channel": "support-4821"
},
"labels": {
"customer_group": "vip"
}
}{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"agent_name": "support-agent",
"status": "stopped",
"occurred_at_ms": 1789913470000,
"created_at": 1789913400,
"ended_at": 1789913470,
"transport": {
"type": "rtc",
"channel": "support-4821"
},
"message": "OK",
"labels": {
"customer_group": "vip"
}
}{
"agent_session_id": "c42ac95tp25py49fc37hp94vm67ht52h",
"agent_name": "support-agent",
"status": "running",
"occurred_at_ms": 1789913401000,
"created_at": 1789913400,
"transport": {
"type": "telephony",
"direction": "outbound",
"from_number": "+14155550100",
"to_number": "+14155550101"
},
"labels": {
"customer_group": "vip"
}
}1002 Session transcripts
Transcript processing for the session finished or failed. This event applies to both transports. The transcript records what the user and the agent said. It isn't the LLM's context window.
status is the result of transcript processing, not the session status. A phone call can end before this event arrives.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
occurred_at_ms | Integer | Yes | When the event happened, as a Unix timestamp in milliseconds. It isn't the NCS delivery time in notifyMs. |
agent_name | String | No | The agent name, when it's known. |
labels | Object<string, string> | No | The labels you passed when you started the session. |
created_at | Integer | Yes | When the session was created, as a Unix timestamp in seconds. |
ended_at | Integer | Yes | When the session ended, as a Unix timestamp in seconds. |
status | String | Yes | The result of transcript processing: completed or failed. |
transcript | Array<Object> | Yes | The spoken segments. Empty when processing failed or no speech was captured. |
transcript[].role | String | Yes | The speaker: user or assistant. |
transcript[].content | String | Yes | The words spoken or played. An interrupted agent segment contains only the part that was played. |
transcript[].turn_id | Integer | Yes | The ID of the turn the segment belongs to. |
transcript[].speech_started_ms | Integer | No | When the speech or playback started, as a Unix timestamp in milliseconds. |
transcript[].speech_ended_ms | Integer | No | When the speech or playback ended, as a Unix timestamp in milliseconds. |
transcript[].metadata | Object | No | The source of the segment in source: greeting, asr, llm, or command. An interrupted segment also contains interrupted, interrupted_ms, and original, the full text the agent planned to say. |
The following example shows a payload for this event:
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"occurred_at_ms": 1789913471000,
"agent_name": "support-agent",
"created_at": 1789913400,
"ended_at": 1789913470,
"status": "completed",
"labels": {
"customer_group": "vip"
},
"transcript": [
{
"role": "assistant",
"content": "How can I help you today?",
"turn_id": 1,
"speech_started_ms": 1789913410000,
"speech_ended_ms": 1789913411000,
"metadata": {
"source": "greeting"
}
},
{
"role": "user",
"content": "Can you tell me a joke?",
"turn_id": 2,
"speech_started_ms": 1789913411200,
"speech_ended_ms": 1789913411800,
"metadata": {
"source": "asr"
}
},
{
"role": "assistant",
"content": "Why did the scarecrow ",
"turn_id": 2,
"speech_started_ms": 1789913412000,
"speech_ended_ms": 1789913412700,
"metadata": {
"source": "llm",
"interrupted": true,
"interrupted_ms": 1789913412700,
"original": "Why did the scarecrow win an award? Because he was outstanding in his field!"
}
},
{
"role": "user",
"content": "Tell me a story instead.",
"turn_id": 3,
"speech_started_ms": 1789913412800,
"speech_ended_ms": 1789913413400,
"metadata": {
"source": "asr"
}
},
{
"role": "assistant",
"content": "Once upon a time in a land far away, there lived a brave knight.",
"turn_id": 3,
"speech_started_ms": 1789913413600,
"speech_ended_ms": 1789913417000,
"metadata": {
"source": "llm"
}
},
{
"role": "assistant",
"content": "Are you still there?",
"turn_id": 4,
"speech_started_ms": 1789913420000,
"speech_ended_ms": 1789913421000,
"metadata": {
"source": "command"
}
}
]
}1003 Session errors
A component of the agent reported an error. The error doesn't necessarily end the session. To find out whether the session ended, use 1001 Session state changed.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
occurred_at_ms | Integer | Yes | When the event happened, as a Unix timestamp in milliseconds. It isn't the NCS delivery time in notifyMs. |
agent_name | String | No | The agent name, when it's known. |
created_at | Integer | Yes | When the session was created, as a Unix timestamp in seconds. |
turn_id | Integer | No | The ID of the turn the error belongs to, if any. |
errors | Array<Object> | Yes | The errors reported in this notification. |
errors[].module | String | Yes | The component that reported the error, for example llm. |
errors[].turn_id | Integer | No | The ID of the turn the error belongs to, if any. |
errors[].code | Integer | Yes | The component's error code. |
errors[].message | String | Yes | A description of the error. |
The following example shows a payload for this event:
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"occurred_at_ms": 1789913430000,
"agent_name": "support-agent",
"created_at": 1789913400,
"turn_id": 2,
"errors": [
{
"module": "llm",
"turn_id": 2,
"code": 503,
"message": "Insufficient balance."
}
]
}1004 Turns finished
The session ended, and its conversation turn data is available. Each turn includes its start, its end, and its latency metrics, if they were collected. The payload can leave out earlier turns. is_truncated tells you whether it did.
Session timestamps are in Unix seconds. Turn boundaries are in Unix milliseconds.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
occurred_at_ms | Integer | Yes | When the event happened, as a Unix timestamp in milliseconds. It isn't the NCS delivery time in notifyMs. |
agent_name | String | No | The agent name, when it's known. |
created_at | Integer | Yes | When the session was created, as a Unix timestamp in seconds. |
ended_at | Integer | Yes | When the session ended, as a Unix timestamp in seconds. |
total_turn_count | Integer | Yes | The total number of turns in the session. |
is_truncated | Boolean | Yes | Whether turns leaves out earlier turns. |
turns | Array<Object> | Yes | The turns in this payload. |
turns[].agent_session_id | String | Yes | The session ID. |
turns[].turn_id | Integer | Yes | The turn ID. |
turns[].start | Object | Yes | The start of the turn and what triggered it. |
turns[].start.started_at_ms | Integer | Yes | When the turn started, as a Unix timestamp in milliseconds. |
turns[].start.type | String | Yes | What started the turn: voice_input, greeting, silence_timeout, api_speak, or api_think. |
turns[].start.metadata | Object | No | Details about what started the turn. |
turns[].end | Object | Yes | The end of the turn and its outcome. |
turns[].end.ended_at_ms | Integer | Yes | When the turn ended, as a Unix timestamp in milliseconds. |
turns[].end.type | String | Yes | How the turn ended: ok, error, ignored, or interrupted. |
turns[].end.metadata | Object | No | Details about how the turn ended. |
turns[].metrics | Object | No | Performance metrics for the turn, if collected. |
turns[].metrics.e2e_latency_ms | Number | No | The end-to-end latency of the turn, in milliseconds. |
turns[].metrics.segmented_latency_ms | Object<string, number> | No | The latency of each component in milliseconds, such as asr_ttlw_ms, llm_ttfb_ms, and tts_ttfb_ms. Components that weren't measured are left out. |
The following example shows a payload for this event:
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"occurred_at_ms": 1789913473000,
"agent_name": "support-agent",
"created_at": 1789913400,
"ended_at": 1789913470,
"total_turn_count": 4,
"is_truncated": true,
"turns": [
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"turn_id": 4,
"start": {
"started_at_ms": 1789913420000,
"type": "silence_timeout"
},
"end": {
"ended_at_ms": 1789913421000,
"type": "ok"
},
"metrics": {
"e2e_latency_ms": 1500,
"segmented_latency_ms": {
"asr_ttlw_ms": 503,
"llm_ttfb_ms": 1104,
"tts_ttfb_ms": 85
}
}
}
]
}1101 RTC token expiring
The agent's RTC token is about to expire. This is a warning only: the agent stays in the channel, and the session continues. If you passed the agent's token when you started the session, update the session with a new transport.token before the token expires.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
occurred_at_ms | Integer | Yes | When the event happened, as a Unix timestamp in milliseconds. It isn't the NCS delivery time in notifyMs. |
agent_name | String | No | The agent name, when it's known. |
transport | Object | Yes | The RTC transport. type is rtc, and channel is the channel name. |
created_at | Integer | Yes | When the session was created, as a Unix timestamp in seconds. |
message | String | Yes | The warning text: RTC token will expire soon. |
The following example shows a payload for this event:
{
"agent_session_id": "a42ac95tp25py49fc37hp94vm67ht52h",
"occurred_at_ms": 1789913450000,
"agent_name": "support-agent",
"created_at": 1789913400,
"transport": {
"type": "rtc",
"channel": "support-4821"
},
"message": "RTC token will expire soon."
}1201 Telephony state
The state of an inbound or outbound phone call changed. Call state is separate from the session status: a running session doesn't mean that the call was answered. Each notification contains the basic call details. You can handle each one on its own, even when notifications arrive out of order.
| Field | Type | Required | Description |
|---|---|---|---|
agent_session_id | String | Yes | The session ID. It's the same as the envelope sid. |
custom_id | String | No | Your own call ID from the SIP X-switch-id header, if any. It isn't the session ID or an idempotency key. |
direction | String | Yes | The call direction: inbound or outbound. |
from_number | String | Yes | The caller's number. |
to_number | String | Yes | The called number. |
occurred_at_ms | Integer | Yes | When the event happened, as a Unix timestamp in milliseconds. It doesn't guarantee ordering. |
state | String | Yes | The call state: starting, calling, ringing, answered, transferred, or ended. A call can skip calling and ringing. Don't assume that a call was answered because it ended. |
hangup_reason | String | No | Why the call ended, as a normalized reason. Present when state is ended. See Hangup reasons. |
hangup_reason_code | String | No | The raw SIP or gateway reason code. Present when state is ended. New values can appear. |
duration_seconds | Number | No | How long the call was connected, in seconds. 0 if it never connected. |
The following example shows a payload for this event:
{
"agent_session_id": "c42ac95tp25py49fc37hp94vm67ht52h",
"custom_id": "customer-call-0001",
"direction": "outbound",
"from_number": "+14155550100",
"to_number": "+14155550101",
"state": "ended",
"occurred_at_ms": 1789913470000,
"hangup_reason": "USER_HANGUP",
"hangup_reason_code": "NORMAL_CLEARING",
"duration_seconds": 60
}Hangup reasons
The following tables list the documented hangup_reason values and the hangup_reason_code values they map to, grouped by whether the call connected. Neither field is a closed list. If you get a hangup_reason_code you don't recognize, hangup_reason is UNKNOWN_REASON.
Reference
For more information, see the following pages:
