Event payload reference
The event shape to send: required fields, accepted fields, and what happens to the rest.
This is the shape to send when no adapter fits your stack. Both keyed endpoints read it, and a webhook can carry it too.
The event shape
One event is one JSON object. Send one, an array of them, or an object with an events array. Bulk import also reads one record per line.
json
{
"thread_id": "contact-4812",
"session_id": "call-99f1",
"type": "message.sent",
"ts": 1755820800000,
"payload": {
"role": "assistant",
"text": "Your order ships tomorrow."
},
"source": {
"platform_msg_id": "msg-7731",
"agent_version": "support-agent-3.2"
}
}The fields
| thread_id | Required. The conversation this belongs to, as your own system keys it |
|---|---|
| session_id | Optional. One call or one chat inside that conversation |
| type | Required. One of message.received, message.sent, tool.called, tool.result, call.started, call.ended, session.note. call.started and call.ended are what make a session a call, so send them only for voice |
| ts | Required. Epoch milliseconds of when the message was created, never when you sent it. A value near 1.7e9 is seconds, and we refuse it |
| channel | Optional. voice or chat. Say it when your stack knows; we infer from the call brackets when you do not. Declaring chat on a call.started is refused |
| source.platform_msg_id | Required. Your own id for this one event |
| source.agent_version | Optional. Which build of the agent produced it |
| payload.role | user, assistant or system |
| payload.text | What was said |
| payload.audio_ref | Where the recording is, for a voice turn |
| payload.tool | For a tool event: name, call_id, arguments, result, error |
The limits
| Timestamp range | Between 2015 and 2100 |
|---|---|
| An id | A string of at most 512 characters |
| Events per payload | 5000 |
ts_precision or clock. The adapter fixes both, at milliseconds and a clock of its own, and an event that sets either one is refused.Chat or call
| A call | Carries call.started or call.ended. Those two events are what make it one, and call.ended closes it |
|---|---|
| A chat | Carries neither. Send the turns and nothing else; the gap after the last one closes it |
| Getting it wrong | A chat sent with call.started is filed as a call, on every screen that splits the two |
The two connector settings
| secret | Optional. An HMAC-SHA256 over the raw body, for the webhook path. With no secret set, the token in the URL is the only guard |
|---|---|
| trust_sender_attribution | Default true. Set it false to ignore human-agent and platform-injected claims from the sender |
With a secret set, send the digest as hex in an x-signature header. An x-hub-signature-256 header works too, and anything before an equals sign is ignored.
Sender attribution
Every turn your deployment produced is tagged, so a reply a person typed is never treated as the agent. An assistant turn is tagged as the agent, a system turn as platform injected, and a user turn is left alone.
You can say so per event with payload.source_tag, set to agent, platform_injected or human_agent. On a human turn, payload.operator_external_id and payload.operator_display_name name who replied.
Send a work identity there, a staff email or a console user id, never a customer's own details.
Set trust_sender_attribution false where you do not control the sender. A claim that excuses the agent is then dropped and the default stands. A claim that blames the agent is kept either way.
Read next
- Conversations · List conversations, and open one with its turns, findings and scores.
- Scores and findings · The number, and which rule broke on which turn. Two endpoints, one reader's job.
- Metrics · Bucketed readings, so you can chart quality beside your own numbers.