# message.sent

A message was sent through the API, the dashboard, or the WhatsApp Business app on Coexistence numbers.

### Sender

`message.sentVia` is one of `human`, `api`, `broadcast`, `sequence`, `workflow`, `comment_automation` or `bulk-api`. It is `null` for sends from the platform's own app and for older messages; treat `null` as unknown, never as human.

### Attribution keys

`automationId`, `workflowId`, `executionId`, `broadcastId` and `sequenceId` are always present. Each is `null` unless that kind of sender produced the message.

### Quote replies

A quote-reply sent through the API carries `metadata.quotedMessageId`. One sent from the WhatsApp Business or Instagram app does not.

### Platform notes

On Instagram and Facebook the attribution can be missing when Meta's echo of a send reaches Zernio before the send is recorded. On WhatsApp, Meta Business Agent replies carry `source: "meta_business_agent"`.




**Payload for `message.sent`:**

- **id** (required) `string`: Stable webhook event ID: the dedupe key, also sent as the X-Zernio-Event-Id header and identical on every retry and redelivery. It identifies the event only, never an account or other resource.
- **profiles**: `WebhookProfiles` - See schema definition
- **test** `boolean`: Always true when present: only a sample sent by POST /v1/webhooks/test with an event carries it. Real deliveries never do.
- **event** (required) `string`: No description - one of: message.sent
- **message** (required) `object`: 
  - **id** (required) `string`: Internal message ID
  - **conversationId** (required) `string`: Internal conversation ID
  - **platform** (required) `string`: Every platform whose outgoing messages Zernio observes. sms is absent on purpose: its carrier receipts update delivery status and never raise message.sent. - one of: instagram, facebook, telegram, whatsapp, twitter, reddit, bluesky, slack, tiktok, imessage
  - **platformMessageId** (required) `string`: Platform's message ID
  - **direction** (required) `string`: No description - one of: incoming, outgoing
  - **text** (required) `string,null`: Message text content
  - **attachments** (required) `array[object]`: 
    - **type** (required) `string`: 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** (required) `string`: Where to fetch the attachment. For outgoing messages this is the media URL as sent. On 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** (required) `object`: **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** (required) `string`: 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** (required) `string` (date-time): 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; some platforms (for example WhatsApp and Telegram) report whole seconds.

Use it as the chronological ordering key. If two messages share the same value, fetch the conversation messages with sortOrder=desc for the deterministic order.

  - **isRead** (required) `boolean`: No description
  - **source** `string`: WhatsApp send origin. Absent on non-WhatsApp platforms.

- whatsapp_business_app: sent from the WhatsApp Business phone app on a Coexistence number.
- cloud_api: sent through Zernio (dashboard, API, or broadcasts).
- meta_business_agent: Meta Business Agent answered on the number.

Says where WhatsApp saw the send come from, not which Zernio surface produced it: read sentVia for that.
 - one of: whatsapp_business_app, cloud_api, meta_business_agent
  - **sentVia** `string,null`: Which Zernio surface produced this message. Same vocabulary as the `source` filter on the inbox analytics endpoints, and the same value a later GET on this message returns.

- `human`: an operator in the Zernio inbox
- `api`: a call to this API
- `broadcast`
- `sequence`
- `workflow`
- `comment_automation`
- `bulk-api`: POST /v1/whatsapp/bulk

Always present, and `null` whenever the lineage is unknown:

- any outgoing message not sent through Zernio (one typed in the platform's own app, a Meta automated reply such as an instant reply or away message, the echo of a click-to-message ad's greeting)
- 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".
 - one of: human, api, broadcast, sequence, workflow, comment_automation, bulk-api, 
  - **automationId** `string,null`: The comment automation that sent this DM (sentVia comment_automation). Null otherwise.
  - **workflowId** `string,null`: The workflow whose run sent this message (sentVia workflow). Null otherwise.
  - **executionId** `string,null`: The workflow run (execution) that sent this message, as returned by the workflow executions endpoints. Null when workflowId is null.
  - **broadcastId** `string,null`: The broadcast that sent this message (sentVia broadcast). Null otherwise.
  - **sequenceId** `string,null`: The sequence whose step sent this message (sentVia sequence). Null otherwise.
- **pricing**: `WhatsAppMessagePricing` - See schema definition
- **billingConversation**: `WhatsAppBillingConversation` - See schema definition
- **conversation** (required): `InboxWebhookConversation` - See schema definition
- **account** (required): `InboxWebhookAccount` - See schema definition
- **metadata** `object`: Platform-specific context for the sent message: a quote-reply reference, a WhatsApp location pin, WhatsApp contact cards, the TikTok message type, or outgoing Instagram/Facebook buttons, quick replies and template.

The key is present only when the send carried some context: it is never null and never an empty object. Read it to tell a location or contact-card message from a text one without a GET on the message.

  - **metaInteractive** `object`: Instagram/Facebook only. The buttons, quickReplies and/or template this outgoing message carried, as sent (tracked urls included when link tracking wrapped a button). Absent when the send carried none.
  - **location** `object`: WhatsApp only. The location pin this message carries, in the same shape the inbox send API accepts.

Present on API sends that passed `location`, and on Coexistence echoes of a pin shared from the WhatsApp Business app.

The message `text` is only the emoji preview (`📍 <name>`); the pin itself lives here.

    - **latitude** `number`: Latitude in decimal degrees.
    - **longitude** `number`: Longitude in decimal degrees.
    - **name** `string`: Location name, when one was given.
    - **address** `string`: Street address, when one was given.
  - **contacts** `array[object]`: WhatsApp only. The contact cards this message carries. The message `text` is only the emoji preview (`👤 <name>`); the cards live here.

- API sends: the `contacts` array exactly as given to the inbox send API (`name`, `phones[].phone` / `type`, `emails[]`).
- Coexistence echoes of a card shared from the WhatsApp Business app: Meta's shape (`phones[].wa_id`, `vcard`).

    Type: `object` with additional properties
  - **quotedMessageId** `string`: `platformMessageId` of the message this send quote-replies to.

- **WhatsApp and Telegram:** present when the reply was sent through Zernio with `replyTo` on the inbox send API. A WhatsApp API send fires `message.sent` off the delivery status, with the quote reference forwarded from the stored send, so it arrives on the same `message.sent` as any WhatsApp send.
- **Instagram echoes:** not delivered. 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:** uses a separate subscription (`message_echoes`) that has not been measured. Treat it as unverified, not supported.
- **WhatsApp Coexistence echoes:** absent. Meta omits the quote context from `smb_message_echoes`, so a reply the operator sent from the WhatsApp Business app has 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.

  - **tiktokMessageType** `string`: TikTok only. The message type as TikTok reports it, forwarded verbatim (for example image, video, sticker, share_post, emoji, reaction, template).

Present on every TikTok DM that is not plain text. Those arrive with text empty and, for image, video, share_post, sticker and emoji, an attachment:

- share_post: type share, with the embed url and payload.videoId.
- sticker/emoji: type sticker, url signed and expiring.

Absent on image sends made through the Zernio API, which carry the image in attachments.

- **timestamp** (required) `string` (date-time): 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.