Overview
Message webhooks push status updates to a URL you control whenever a message reaches a terminal state. Configure them in your workspace settings — Tuco handles delivery automatically.Webhook calls are POST requests with a JSON body. All events share the same
base fields; each event type adds its own specific fields. Outgoing message
events are fired when a message is sent (accepted by Tuco), not when
delivery is confirmed on the recipient’s device.
Event types
| Event | When | Payload message type | Status field |
|---|---|---|---|
message.sent | Message was successfully sent (accepted by Tuco) | Object (full message doc) | "sent" |
message.failed | Message failed after all retries (technical error) | Object (full message doc) | "failed" |
message.fallback | Recipient has no iMessage; primary channel unavailable | Object (full message doc) | "fallback" |
message.opened | Recipient opened/read the message (read receipt; when supported) | Object (full message doc with readAt) | — |
message.reply | A lead or contact replied to a message you sent | String (reply text only) | — |
message.reply
Sent when a new reply from a lead or contact is recorded. Use it to update your CRM, trigger automations, or log conversations.
Payload shape: Top-level flat fields; data.reply is identical to one item in GET /api/replies (same keys, same types). Reply text is top-level message (string). parentMessages is always present (array; possibly empty).
Top-level fields
| Field | Description |
|---|---|
event | "message.reply" |
timestamp | When the webhook was sent (ISO UTC) |
workspaceId | Workspace that owns the conversation |
messageId | ID of the reply message (the one they sent) |
leadId | Lead ID when the reply is linked to a lead. Omitted if no lead could be matched. |
firstName | Lead first name (when lead is present). Backward-compat flat field. |
lastName | Lead last name (when lead is present). Backward-compat flat field. |
phone | Lead phone (when lead is present). Backward-compat flat field. |
listId | Lead list ID (when lead is present). Backward-compat flat field. |
campaignId | Campaign ID when the reply is tied to a campaign. |
campaignName | Campaign name when tied to a campaign. |
fromLineId | Line ID that received the reply. Backward-compat flat field. |
recipientName | Display name (lead name or recipient). Backward-compat flat field. |
repliedAtUtc | When the reply was received (UTC ISO 8601). Always present. |
parentMessages | Last 2 outbound messages you sent to this lead. Always present (array; empty if none). Same shape as GET /api/replies. |
message | Reply text only (string) — what they replied with. |
originalMessageId | ID of the message they replied to (when known) |
originalMessage | Original outbound message object (when known) |
lead | Full lead object when available. Omitted if no lead could be matched. |
message (reply text only)
For message.reply, message is the reply text only (string) — what the lead or contact replied with.
data.reply — matches GET /api/replies item exactly
data.reply has the exact same shape as one element of GET /api/replies replies array:
| Field | Description |
|---|---|
leadId | Lead ID (string) |
messageId | Reply message ID |
respondedAtUtc | When the reply was received (UTC ISO) |
recipientEmail | Recipient email or undefined |
recipientPhone | Recipient phone or undefined |
name | Display name (lead name or recipient) |
parentMessages | Array of { messageId, message, sentAtUtc, batchId, stepIndex } (same as API) |
GET /api/replies response replies[n].
Example payload:
message.sent
Sent when a message has been sent (accepted by Tuco). This is the only
outgoing-message event fired to your webhook; Tuco does not fire a separate
event when delivery is confirmed on the recipient’s device.
Payload: The message field is the full message object (not a string), with _id, message, messageType, status, fromLineId, recipientPhone, recipientEmail, recipientName, leadId, workspaceId, createdByUserId, sentAt, createdAt, updatedAt, etc. Top-level also includes leadId, lead (full lead when available), campaignId, campaignName, and integration IDs.
Full example body
"message.sent"Tuco message ID
Full message document (not string)
Lead ID when known
Full lead when available
GHL contact ID for integrations
GHL location ID for integrations
When the webhook was fired (ISO UTC)
message.failed
Sent when a message has exhausted all retry attempts or encountered an
unrecoverable error (e.g. availability API failure). The message field is the full message object (includes errorMessage when set). data may include error (reason string).
Full example body
"message.failed"Full message document (includes errorMessage)
Human-readable explanation of the failure
message.fallback
Sent when Tuco determines that the primary channel (e.g. iMessage) is not
available for this recipient. This is a business outcome, not a technical
failure. The message field is the full message object. data includes reason and optionally checkedAddresses (string[]).
Full example body
"message.fallback"Full message document (status fallback)
Human-readable explanation
All addresses Tuco checked for iMessage availability before deciding fallback.
Useful for debugging and auditing.
Common patterns
Auto-send SMS
Trigger an SMS via your own Twilio/fallback when you receive
message.fallback.
Or enable Tuco’s built-in fallback SMS on the workspace.Tag lead in CRM
Flag the lead as “no iMessage” so future campaigns pick the right channel
automatically.
message.opened
Sent when the recipient has opened/read the message (read receipt). Fired where the channel supports read receipts. Use it to track engagement or trigger follow-ups.
Payload: Same top-level shape as message.sent. The message field is the full message object (not a string), including readAt (ISO 8601 UTC) when the message was read. data may include readAt.
Full example body
"message.opened"Full message document including readAt (ISO UTC when read)
When the message was opened (ISO UTC)
Shared fields (all events)
Every webhook payload includes these base fields:| Field | Description |
|---|---|
event | Event type: message.sent, message.failed, message.fallback, message.opened, or message.reply |
timestamp | When the webhook was sent (ISO UTC) |
workspaceId | Your workspace ID |
messageId | Tuco message ID — use as your primary key (for reply, this is the reply message ID) |
message | For sent/failed/fallback/opened: full message object. For reply: string (reply text only) |
leadId | Lead involved (when known) |
lead | Full lead object when available |
ghlContactId, ghlLocationId, hsPortalId, hsContactId | Integration IDs (nullable) |
data | Event-specific data (e.g. reason, checkedAddresses for fallback; reply, repliedAtUtc, parentMessages for reply; readAt for opened) |
Consuming webhooks safely
Use messageId + event as your key
Make updates idempotent. If you’ve already processed a
(messageId, event) pair, ignore duplicates.Move status forward only
Only update your local status in the forward direction:
queued → sent → delivered. Never move backwards.Return 200 quickly
Tuco expects a
2xx response within a few seconds. Do heavy processing
asynchronously after acknowledging the webhook.