---
title: "Message sent event"
method: POST
path: "message.sent"
tags: ["Webhook Events"]
---

# Message sent event

`POST message.sent` (webhook)

Fired when a message is sent via the API, or from the WhatsApp Business app on
Coexistence numbers. Sends that carry platform-specific context deliver it under
`metadata`, so a quote-reply sent through the API arrives with
`metadata.quotedMessageId` and mirroring CRMs can thread it without a lookup.
Which surfaces actually carry that reference is documented on
`WebhookPayloadMessageSent.metadata.quotedMessageId`; a quote-reply sent from
the WhatsApp Business or Instagram app is not one of them.

## Payload

- WebhookPayloadMessageSent — Webhook payload for message sent events (fired when a message is sent via the API, or from the WhatsApp Business app on Coexistence numbers)
  - `id` string, required — Stable webhook event ID
  - `event` 'message.sent', required
  - `message` object, required
    - `id` string, required — Internal message ID
    - `conversationId` string, required — Internal conversation ID
    - `platform` 'instagram' | 'facebook' | 'telegram' | 'whatsapp' | 'twitter' | 'reddit' | 'bluesky' | 'slack', required — Every platform whose outgoing messages Zernio observes. sms is absent on purpose: its carrier receipts update delivery status and never raise message.sent.
    - `platformMessageId` string, required — Platform's message ID
    - `direction` 'incoming' | 'outgoing', required
    - `text` string, nullable, required — Message text content
    - `attachments` object[], required
      - `type` string, required — Attachment type (image, video, file, sticker, audio, share)
      - `originalType` string — Instagram and Facebook only, and present only when it differs from `type`. Meta's own attachment type before Zernio normalized it. See the same field on message.received for the full mapping.
      - `url` string, required — Where to fetch the attachment. For outgoing messages this is the media URL as sent, so for WhatsApp it is the URL you supplied when publishing (WhatsApp sends media by link), not a Zernio endpoint, and it needs no Zernio credentials. Contrast the inbound direction: `message.received` attachment URLs on WhatsApp point at the authenticated `GET /v1/whatsapp/media/{mediaId}`. As on `message.received`, webhook attachments carry no `refreshUrl`: that field is stamped only on the REST read. Resolve Instagram and Facebook media through `GET /v1/inbox/conversations/{conversationId}/messages/{messageId}/attachments/{index}?accountId={accountId}`.
      - `payload` object — Additional attachment metadata
    - `sender` object, required — **On this event the sender is your own business, not the person you are talking to.** `id` is the Zernio account id and `name`, `username` and `picture` are that connected account's own profile. Do not read these to name or update a contact: doing so on an echo relabels the customer's record with your business name. The other party is `conversation.participantId` / `participantName` / `participantUsername`, which are populated in both directions.
      - `id` string, required — The Zernio account id of the connected account that sent the message, not a contact id.
      - `contactId` string — Always omitted on this event: the sender is the business, not a contact. Use conversation.contactId to join back to the CRM Contact.
      - `name` string — Display name of your connected account.
      - `username` string — Username of your connected account.
      - `picture` string — Profile picture of your connected account.
    - `sentAt` string, date-time, required — When the message was sent, as reported by the platform and passed through unmodified. Full ISO 8601 date-time: Instagram and Facebook carry millisecond precision, while some platforms (for example WhatsApp and Telegram) report whole seconds. Use this field as the chronological ordering key. If two messages share the same value, fetch the conversation messages with sortOrder=desc for the deterministic order.
    - `isRead` boolean, required
    - `source` 'whatsapp_business_app' | 'cloud_api' — WhatsApp send origin. whatsapp_business_app when sent from the WhatsApp Business phone app on a Coexistence number; cloud_api when sent through Zernio (dashboard, API, or broadcasts). Absent on non-WhatsApp platforms. Says where WhatsApp saw the send come from, not which Zernio surface produced it: read sentVia for that.
    - `sentVia` 'human' | 'api' | 'broadcast' | 'sequence' | 'workflow' | 'comment_automation' | 'bulk-api' | 'null', nullable — Which Zernio surface produced this message: `human` (an operator in the Zernio inbox), `api` (a call to this API), `broadcast`, `sequence`, `workflow`, `comment_automation`, or `bulk-api` (POST /v1/whatsapp/bulk). Same vocabulary as the `source` filter on the inbox analytics endpoints, and the same value a later GET on this message returns. Always present, and `null` whenever the lineage is unknown: a message sent from the platform's own app, and every message stored before this field shipped (2026-08). Existing messages are NOT backfilled, so treat `null` as "unknown", never as "sent by a human".
  - `conversation` InboxWebhookConversation, required — The conversation context included in inbox webhook payloads.
    - `id` string, required
    - `platformConversationId` string, required
    - `participantId` string
    - `participantName` string
    - `participantUsername` string
    - `participantPicture` string
    - `status` 'active' | 'archived', required
    - `contactId` string — Zernio CRM Contact ID for the participant, when one exists. Resolved by joining `participantId` to the ContactChannel collection. Best-effort: omitted when no channel matches or `participantId` is absent. Lets integrators join any inbox webhook back to the CRM Contact without needing to look at the sender — which matters for outgoing and delivery-status events whose sender is the business.
  - `account` InboxWebhookAccount, required — The account context included in inbox webhook payloads.
    - `id` string, required — Social account ID
    - `accountId` string — Social account ID (same value as id). Canonical field so consumers can filter every webhook event on one field (e.g. route staging vs production by account). id is kept for backward compatibility.
    - `profileId` string — Zernio profile (workspace) ID this account belongs to. Use it to route or filter inbox webhooks by workspace. This is the profile ID only, not its name (resolve the name via the API with this ID). Optional; omitted on the shared WhatsApp sandbox account and when the account has no resolvable profile.
    - `platform` string, required
    - `username` string, required
    - `displayName` string
  - `metadata` object — Platform-specific context for the sent message. The key is present only when the send carried some context, and absent otherwise: it is never null and never an empty object.
    - `quotedMessageId` string — `platformMessageId` of the message this send is a quote-reply to. Present when the reply was sent through Zernio with `replyTo` on the inbox send API (WhatsApp and Telegram). A WhatsApp API send fires its `message.sent` off the delivery status, and the quote reference is forwarded from the stored send there, so it arrives on the same `message.sent` as any other WhatsApp send. Not delivered on Instagram echoes. Zernio forwards `reply_to.mid` whenever Meta puts it on an echo, but on Instagram Meta does not send it, so a reply the operator quoted in the Instagram app arrives with no `quotedMessageId`. Facebook Messenger rides a separate subscription (`message_echoes`) and has not been measured, so treat it as unverified rather than supported. Absent on WhatsApp Coexistence echoes. Meta omits the quote context from `smb_message_echoes`, so a reply the operator sent from the WhatsApp Business app arrives with no `quotedMessageId` even though WhatsApp shows it as a quote-reply. Do not read the absence of this field as "not a reply".
    - `threadTs` string — Slack only. Parent thread ts of the sent message. Pass it back as `replyTo` on the inbox send API to keep replying inside the thread.
  - `timestamp` string, date-time, required — UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.

## Acknowledgement `200`

Webhook received successfully

---

[API](https://skmtc.dev/zernio/apis/zernio-api.md) · [All operations](https://skmtc.dev/zernio/apis/zernio-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/zernio/zernio-api/revisions/017dff7e4324/schema)
