OdyDocs

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

EventFires when
message.receivedAn inbound SMS/MMS is stored
message.deliveredAn outbound message is accepted by the carrier, and again on each delivery-status update
message.failedThe carrier reports an outbound message failed
call.ringingAn inbound call reaches one of your numbers, before anyone answers
call.answeredSomeone picks up: a teammate, the AI agent, or the person you called
call.forwardedAn inbound call is sent on to an outside phone number
call.completedAn answered call ends (by a teammate or the AI agent)
call.missedAn inbound call ends unanswered
call.recording.completedA call recording is stored
call.voicemail.completedA voicemail is stored
call.transcript.completedA call or voicemail transcript is ready
call.summary.completedThe AI summary of a call is ready
call.flow.startedAn inbound call enters an IVR call flow
call.flow.completedA call flow reaches its final step
call.flow.abortedA call flow stops early
contact.updatedA contact is created or edited in the app, the API or MCP
contact.deletedA 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 callOutbound call
call.ringing → call.answered or call.forwarded → call.completed / call.missed → call.recording.completed, call.transcript.completed, call.summary.completedcall.answered → call.completed / call.missed → call.recording.completed, call.transcript.completed
Correlating live and finished events

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:

  • conversationId uuid — The thread. Fetch it with GET /conversations/:id.
  • inboxId uuid — The inbox the thread belongs to.
  • activityId uuid — The message, call or voicemail inside the thread.
  • numberE164 string — Your Ody number involved.
  • telnyxCallId string — 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.

JSON
{
  "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.

  • deliveryStatus string — Present on carrier status updates, for example sent or delivered. Absent on the first event at send time.
JSON
{
  "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).

  • deliveryStatus string — The carrier's failure status, for example sending_failed or delivery_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.

  • numberE164 string — The Ody number that was called.
  • from string | null — The caller, in E.164. null when the caller withheld their number.
  • callerName string | null — The caller name the carrier delivered (CNAM), if any.
  • telnyxCallId string | null — Join key for the later call.answered / call.completed / call.missed.
  • direction string — Always inbound.
  • routedBy string — number when the number's own settings route the call, flow when a call flow (phone menu) takes it. Flow calls also send call.flow.started.
JSON
{
  "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:

answeredByMeaningExtra fields
agentThe AI agent took an inbound call. Sent the moment the agent picks up.agentId
memberA teammate answered an inbound call on the Ody app.memberUid — the teammate's user ID
calleeThe person you dialed answered an outbound call placed from the app.conversationId, inboxId, activityId — the call already has its timeline entry
  • answeredBy string — agent, member or callee.
  • direction string — inbound or outbound.
  • numberE164 string | null — Your Ody number. For member answers it is included when the workspace has one number or the teammate has an assigned line; otherwise it is omitted rather than guessed, and call.completed carries the definitive number.
  • from string | null — The caller (inbound).
  • telnyxCallId string | null — Join key.
JSON
{
  "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.

  • forwardedTo string — The outside number the call went to, in E.164.
  • numberE164 string, from string | null, telnyxCallId string | null, direction string — As in call.ringing.
JSON
{
  "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.

JSON
{
  "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.

JSON
{
  "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.

  • numberE164 string — The number that was called.
  • flowId string, flowName string, flowVersion integer — Which flow, and which published version.
  • telnyxCallId string | null
  • terminal string — completed only: the final step type, for example ring, voicemail, forward, astra or hangup.
  • reason string — aborted only: why the flow stopped.
  • nodeId string — completed and aborted: 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.

  • contactId uuid — The contact.
  • change string — created or updated.
  • fields string[] — updated only: 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.
JSON
{
  "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.

JSON
{
  "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.

Last updated Wed Oct 14 2026 00:00:00 GMT+0000 (Coordinated Universal Time)Something wrong on this page? Tell us