Voicetta
    Voicetta.

    Voicetta Developers

    Message Events

    The message.received and message.sent events for SMS and WhatsApp: full payload, the reply field, the source vocabulary, delivery timing, and why text costs are indicative.

    Message Events

    Two events cover every SMS and WhatsApp message your assistants handle. They are the live feed: what is being said, right now.

    EventFires for
    message.receivedA message that arrived from a customer, together with the reply we sent back
    message.sentA message we sent on our own initiative, with no incoming message behind it

    Everything about headers, signatures, retries and the shared envelope is on the webhook reference. This page is the payload and the two or three behaviours specific to text.

    Why received and sent are separate events

    An arriving message and an outgoing one are not the same thing, and one record is never both.

    When a customer texts you, we create one record holding their message and the reply our assistant generated in the same turn. That is message.received, and the reply travels with it in a named reply field rather than as an unlabelled second entry.

    When we start the conversation — a confirmation the voice assistant sent mid-call, or a recovery follow-up after a conversation went badly — there is nothing incoming to report. That is message.sent, and it has no reply.

    Collapsing the two into one event would mean every receiver inspecting a direction field to work out which half of the payload to trust. Two names say it up front.

    message.received

    json
    { "event": "message.received", "event_id": "b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31", "version": 1, "occurred_at": "2026-08-20T14:12:00+00:00", "message": { "message_id": "b71e9d02-4a6c-4f18-9d3e-7c2b5a4e8f31", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "channel": "whatsapp", "direction": "inbound", "thread_id": "c82fa013-5b7d-4029-8e4f-8d3c6b5f9a42", "source": "inbound", "occurred_at": "2026-08-20T14:12:00+00:00", "content": "Do you have a room free on Friday?", "provider_message_id": "SM8f3c1a2b4d5e6f70", "reply": { "content": "Yes, we have a double available. Shall I hold it?", "provider_message_id": "SM1a2b3c4d5e6f7080", "timestamp": "2026-08-20T14:12:04+00:00" }, "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "name": "Jane Texter", "phone": "+15551234567" } } }

    message.sent

    json
    { "event": "message.sent", "event_id": "d93ba124-6e8f-4130-9a5f-9e4d7c6b0a53", "version": 1, "occurred_at": "2026-08-20T10:03:12+00:00", "message": { "message_id": "d93ba124-6e8f-4130-9a5f-9e4d7c6b0a53", "workspace_id": "2a1f8e7d-4c3b-4a29-9f18-6d5e4c3b2a19", "assistant_id": "7c6b5a49-3e2d-4c1b-8a97-5f4e3d2c1b09", "assistant_name": "Front Desk", "channel": "sms", "direction": "outbound", "thread_id": "e04cb235-7f90-4241-8b60-af5e8d7c1b64", "source": "voice_tool", "occurred_at": "2026-08-20T10:03:12+00:00", "content": "Your booking reference is BK-4471. See you Friday.", "provider_message_id": "SM2b3c4d5e6f708190", "customer": { "customer_id": "5d4c3b2a-1f09-4e8d-7c6b-5a493e2d1c0b", "phone": "+15551234567" } } }

    Reading the fields

    • content is always present. It is the event. A notification that something was said, without what was said, would not be worth delivering.
    • channel is sms or whatsapp.
    • direction is inbound on received and outbound on sent. It is redundant with the event name on purpose, so a stored payload is self-describing without its headers.
    • thread_id is the conversation this message belongs to, and the same value the thread event reports when that conversation finishes. Group on it to reconstruct a conversation from individual messages.
    • provider_message_id is the carrier's own id, for cross-referencing against a Twilio or Telnyx log. It is absent when the provider gave us none.
    • reply appears on message.received only, and only when the assistant answered in the same turn. A received message with no reply means no reply was generated — worth surfacing rather than swallowing.

    source: who started it

    ValueMeaning
    inboundThe customer messaged first
    voice_toolThe voice assistant sent it during a phone call, typically a confirmation
    evaluation_follow_upA recovery follow-up after a conversation was graded as having a gap
    testSent from the dashboard's test panel

    The first three are real traffic and are all delivered. test never is — test records are excluded from webhooks and from the read API alike, so nothing you built for production sees them. The value is documented because it appears in the dashboard, not because you will receive one.

    Treat an unfamiliar source as "some other origin" rather than as an error. New ones get added as the product grows, and that is an addition rather than a breaking change.

    Timing

    We poll for new messages every 15 seconds, so a message reaches you within roughly 15 to 30 seconds of being sent or received.

    That is live enough to drive an activity feed and not fast enough to build a chat client on. It is also unordered: deliveries run concurrently, so the second message of an exchange can arrive before the first. Order on occurred_at.

    Costs are indicative, not billed

    Tick the costs block and each message carries a message_cost and a currency.

    That number is a rate, applied when the payload is built. Unlike the charged amount on a voice call, text messaging is not debited from your wallet per message, so nothing here reconciles against an invoice. It is the right figure for showing a client roughly what their messaging is worth, and the wrong figure for anything you bill on. Your invoice is the source of truth for billing.

    What a message payload does not carry

    No summary, no evaluation verdict, no recording, no latency measurement and no configuration snapshot. All of those describe a finished conversation or an audio pipeline, and a single message is neither.

    Those blocks are absent from the payload rather than sent as nulls or empty arrays. If you want the graded view of the conversation, that is the thread event — the two are complements, joined by thread_id.

    Reading messages instead of receiving them

    GET /v1/messages returns the same objects on demand, with filters for date, assistant, channel, direction and thread_id. Full parameters in the read API reference.

    This is also how you get messages from before you ticked the event, since enabling an event is not retroactive.