Event types
Every webhook event Ody can send, when it fires, and the fields in its data object.
Subscribe an endpoint to individual types, or to ["*"] for every current and future type. GET /v1/api/webhook-events returns the live catalog, and the SDKs export it as WEBHOOK_EVENT_TYPES with a typed data shape per event.
#Summary
| Event | Fires when |
|---|---|
message.received | An inbound SMS/MMS is stored |
message.delivered | An outbound message is accepted by the carrier, and again on each delivery-status update |
message.failed | The carrier reports an outbound message failed |
call.ringing | An inbound call reaches one of your numbers, before anyone answers |
call.answered | Someone picks up: a teammate, the AI agent, or the person you called |
call.forwarded | An inbound call is sent on to an outside phone number |
call.completed | An answered call ends (by a teammate or the AI agent) |
call.missed | An inbound call ends unanswered |
call.recording.completed | A call recording is stored |
call.voicemail.completed | A voicemail is stored |
call.transcript.completed | A call or voicemail transcript is ready |
call.summary.completed | The AI summary of a call is ready |
call.flow.started | An inbound call enters an IVR call flow |
call.flow.completed | A call flow reaches its final step |
call.flow.aborted | A call flow stops early |
contact.updated | A contact is created or edited in the app, the API or MCP |
contact.deleted | A contact is deleted |
#The life of a call
Call events arrive in this order. The first three are live signals, sent while the call is still going on; the rest describe the finished call and carry a conversationId.
| Inbound call | Outbound call |
|---|---|
call.ringing → call.answered or call.forwarded → call.completed / call.missed → call.recording.completed, call.transcript.completed, call.summary.completed | call.answered → call.completed / call.missed → call.recording.completed, call.transcript.completed |
call.ringing, call.answered and call.forwarded fire before the call has a timeline entry, so they have no conversationId. Use telnyxCallId to join them to the later call.completed or call.missed, which carries both IDs.
Most events share these data fields:
conversationIduuid — The thread. Fetch it withGET /conversations/:id.inboxIduuid — The inbox the thread belongs to.activityIduuid — The message, call or voicemail inside the thread.numberE164string — Your Ody number involved.telnyxCallIdstring — For call events: the carrier's call ID. Join live and finished events on it, and quote it when talking to support.
#Messages
#message.received
An inbound SMS or MMS was stored in a thread. Fetch the body and media from the conversation.
{
"type": "message.received",
"data": {
"numberE164": "+15125550100",
"conversationId": "c4f1a2b3-…",
"inboxId": "1e2d3c4b-…",
"activityId": "a1b2c3d4-…",
"contactId": "8b1d6f3e-…"
}
}#message.delivered
Fires when an outbound message (from the app, the API, MCP or the AI) is accepted by the carrier, and again each time the carrier reports progress. Status only ever moves forward, so a late sent never overwrites delivered.
deliveryStatusstring — Present on carrier status updates, for examplesentordelivered. Absent on the first event at send time.
{
"type": "message.delivered",
"data": { "conversationId": "c4f1…", "inboxId": "1e2d…", "activityId": "5e7f…", "deliveryStatus": "delivered" }
}#message.failed
The carrier reported that an outbound message could not be delivered (for example, the number is unreachable or the recipient opted out).
deliveryStatusstring — The carrier's failure status, for examplesending_failedordelivery_failed.
#Calls
#call.ringing
An inbound call reached one of your numbers and your phones (or the AI agent, or the call flow) are about to take it. This is the earliest signal Ody has about a call: use it to open the caller's record in your CRM while the phone is still ringing.
Sent once per inbound call, before routing. Not sent for outbound calls, or when the workspace is suspended.
numberE164string — The Ody number that was called.fromstring | null — The caller, in E.164.nullwhen the caller withheld their number.callerNamestring | null — The caller name the carrier delivered (CNAM), if any.telnyxCallIdstring | null — Join key for the latercall.answered/call.completed/call.missed.directionstring — Alwaysinbound.routedBystring —numberwhen the number's own settings route the call,flowwhen a call flow (phone menu) takes it. Flow calls also sendcall.flow.started.
{
"type": "call.ringing",
"data": {
"numberE164": "+15125550100",
"from": "+15125559999",
"callerName": "ADA LOVELACE",
"telnyxCallId": "v3:6aL…",
"direction": "inbound",
"routedBy": "number"
}
}#call.answered
Someone picked up. answeredBy tells you who, and the other fields depend on it:
answeredBy | Meaning | Extra fields |
|---|---|---|
agent | The AI agent took an inbound call. Sent the moment the agent picks up. | agentId |
member | A teammate answered an inbound call on the Ody app. | memberUid — the teammate's user ID |
callee | The person you dialed answered an outbound call placed from the app. | conversationId, inboxId, activityId — the call already has its timeline entry |
answeredBystring —agent,memberorcallee.directionstring —inboundoroutbound.numberE164string | null — Your Ody number. Formemberanswers it is included when the workspace has one number or the teammate has an assigned line; otherwise it is omitted rather than guessed, andcall.completedcarries the definitive number.fromstring | null — The caller (inbound).telnyxCallIdstring | null — Join key.
{
"type": "call.answered",
"data": {
"numberE164": "+15125550100",
"from": "+15125559999",
"telnyxCallId": "v3:6aL…",
"direction": "inbound",
"answeredBy": "member",
"memberUid": "uid_ana"
}
}Calls the AI agent places for you through POST /calls do not send call.answered; their first event is call.completed.
#call.forwarded
An inbound call is being connected to an outside phone number: the number is set to forward, the caller pressed a menu key that forwards, or a call flow reached a forward step. No call.answered follows for the outside line. When the call ends you get call.completed (it was picked up) or call.missed.
forwardedTostring — The outside number the call went to, in E.164.numberE164string,fromstring | null,telnyxCallIdstring | null,directionstring — As incall.ringing.
{
"type": "call.forwarded",
"data": {
"numberE164": "+15125550100",
"from": "+15125559999",
"telnyxCallId": "v3:6aL…",
"direction": "inbound",
"forwardedTo": "+15125550001"
}
}#call.completed
An answered call ended. When the AI agent handled the call, data.answeredBy is "agent". The recording, transcript and summary follow as separate events.
{
"type": "call.completed",
"data": {
"conversationId": "c4f1…",
"inboxId": "1e2d…",
"activityId": "9d8c…",
"numberE164": "+15125550100",
"telnyxCallId": "v3:6aL…",
"answeredBy": "agent"
}
}#call.missed
An inbound call ended without being answered (no voicemail, or before one was left). Same fields as call.completed, without answeredBy.
#call.recording.completed
A call recording was stored on the call activity. The signed recording URL is on the activity, which you get from GET /conversations/:id.
#call.voicemail.completed
A caller left a voicemail. The audio is on the activity; the transcript follows as call.transcript.completed.
#call.transcript.completed
A speaker-labelled transcript is ready for a call or voicemail. Read it from the activity's callTranscript.
#call.summary.completed
The AI summary of a call is ready. For calls the AI agent handled, data.summaryText carries the summary inline. For other calls, read it from the activity.
{
"type": "call.summary.completed",
"data": {
"conversationId": "c4f1…",
"activityId": "9d8c…",
"telnyxCallId": "v3:6aL…",
"summaryText": "Ada asked to move Friday's appointment to Monday 10am; confirmed and texted the new time."
}
}#Call flow events
Fired by IVR call flows (phone menus) built in the app. Use them to measure menu drop-off or trigger follow-ups.
numberE164string — The number that was called.flowIdstring,flowNamestring,flowVersioninteger — Which flow, and which published version.telnyxCallIdstring | nullterminalstring —completedonly: the final step type, for examplering,voicemail,forward,astraorhangup.reasonstring —abortedonly: why the flow stopped.nodeIdstring —completedandaborted: the step it ended on.
#Contacts
Contact events carry only the contact's ID, never the record itself, so an endpoint that is only meant to learn "something changed" is never sent names and phone numbers. Fetch the current state with GET /contacts/:id.
They fire for deliberate changes made in the app, through the API or through MCP. They do not fire for the placeholder contacts Ody creates automatically when an unknown number calls or texts, nor for bulk CSV imports and CRM syncs: a 2,000-row import would otherwise send 2,000 deliveries. Read the result of an import through the Contacts API instead.
#contact.updated
A contact was created or edited.
contactIduuid — The contact.changestring —createdorupdated.fieldsstring[] —updatedonly: the top-level fields the caller sent (for example["lastName", "tags"]). Sending a field with its existing value still lists it, so treat this as "what was touched", not a diff.
{
"type": "contact.updated",
"data": { "contactId": "8b1d6f3e-…", "change": "updated", "fields": ["lastName", "tags"] }
}#contact.deleted
A contact was deleted. GET /contacts/:id now returns 404, so remove or archive your copy.
{
"type": "contact.deleted",
"data": { "contactId": "8b1d6f3e-…" }
}#The test event
POST /webhooks/:id/test sends {"type": "ping", "data": {"message": "Test delivery from Ody"}}, signed like a real event. Its ID starts with evt_test_. ping isn't in the catalog and you can't subscribe to it, so make sure your handler doesn't error on unknown types.