Skip to main content

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

EventWhenPayload message typeStatus field
message.sentMessage was successfully sent (accepted by Tuco)Object (full message doc)"sent"
message.failedMessage failed after all retries (technical error)Object (full message doc)"failed"
message.fallbackRecipient has no iMessage; primary channel unavailableObject (full message doc)"fallback"
message.openedRecipient opened/read the message (read receipt; when supported)Object (full message doc with readAt)
message.replyA lead or contact replied to a message you sentString (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

FieldDescription
event"message.reply"
timestampWhen the webhook was sent (ISO UTC)
workspaceIdWorkspace that owns the conversation
messageIdID of the reply message (the one they sent)
leadIdLead ID when the reply is linked to a lead. Omitted if no lead could be matched.
firstNameLead first name (when lead is present). Backward-compat flat field.
lastNameLead last name (when lead is present). Backward-compat flat field.
phoneLead phone (when lead is present). Backward-compat flat field.
listIdLead list ID (when lead is present). Backward-compat flat field.
campaignIdCampaign ID when the reply is tied to a campaign.
campaignNameCampaign name when tied to a campaign.
fromLineIdLine ID that received the reply. Backward-compat flat field.
recipientNameDisplay name (lead name or recipient). Backward-compat flat field.
repliedAtUtcWhen the reply was received (UTC ISO 8601). Always present.
parentMessagesLast 2 outbound messages you sent to this lead. Always present (array; empty if none). Same shape as GET /api/replies.
messageReply text only (string) — what they replied with.
originalMessageIdID of the message they replied to (when known)
originalMessageOriginal outbound message object (when known)
leadFull 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:
FieldDescription
leadIdLead ID (string)
messageIdReply message ID
respondedAtUtcWhen the reply was received (UTC ISO)
recipientEmailRecipient email or undefined
recipientPhoneRecipient phone or undefined
nameDisplay name (lead name or recipient)
parentMessagesArray of { messageId, message, sentAtUtc, batchId, stepIndex } (same as API)
No extra fields. Same as GET /api/replies response replies[n]. Example payload:
{
  "event": "message.reply",
  "timestamp": "2025-03-02T15:00:00.000Z",
  "workspaceId": "org_xxx",
  "messageId": "674abc...",
  "leadId": "667f1f77bcf86cd799439012",
  "firstName": "Jane",
  "lastName": "Doe",
  "phone": "+12025551234",
  "listId": "674list...",
  "campaignId": "674...",
  "campaignName": "Q1 Outreach",
  "fromLineId": "699f704bc405fae6ca299a08",
  "recipientName": "Jane Doe",
  "repliedAtUtc": "2025-03-02T14:59:58.000Z",
  "parentMessages": [
    { "messageId": "674def...", "message": "Hi Jane, we have an offer...", "sentAtUtc": "2025-03-01T10:00:00.000Z", "batchId": "campaign_674..._0", "stepIndex": 0 }
  ],
  "message": "Thanks, I'm interested!",
  "data": {
    "reply": {
      "leadId": "667f1f77bcf86cd799439012",
      "messageId": "674abc...",
      "respondedAtUtc": "2025-03-02T14:59:58.000Z",
      "recipientEmail": "jane@example.com",
      "recipientPhone": null,
      "name": "Jane Doe",
      "parentMessages": [
        { "messageId": "674def...", "message": "Hi Jane, we have an offer...", "sentAtUtc": "2025-03-01T10:00:00.000Z", "batchId": "campaign_674..._0", "stepIndex": 0 }
      ]
    },
    "repliedAtUtc": "2025-03-02T14:59:58.000Z",
    "parentMessages": [ { "messageId": "674def...", "message": "Hi Jane, we have an offer...", "sentAtUtc": "2025-03-01T10:00:00.000Z", "batchId": "campaign_674..._0", "stepIndex": 0 } ]
  },
  "originalMessageId": "674def...",
  "lead": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com" }
}

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

{
  "event": "message.sent",
  "timestamp": "2026-03-12T22:57:03.000Z",
  "workspaceId": "org_3AZs4H8UsfFxVFONr6H4K75okaG",
  "messageId": "69b344bc6dae1d942ece4d0e",
  "message": {
    "_id": "69b344bc6dae1d942ece4d0e",
    "message": "Here's your document.",
    "messageType": "imessage",
    "status": "sent",
    "fromLineId": "69ab427ac1feb77d7f46462e",
    "recipientPhone": "+919042956129",
    "recipientEmail": "goforbg@icloud.com",
    "recipientName": "BG Real",
    "leadId": "69b2c41d352a78479d2c623b",
    "workspaceId": "org_3AZs4H8UsfFxVFONr6H4K75okaG",
    "createdByUserId": "user_3AZrxEi8CR2uMq9simYrwcJpfsL",
    "sentAt": "2026-03-12T22:57:02.892Z",
    "createdAt": "2026-03-12T22:57:00.856Z",
    "updatedAt": "2026-03-12T22:57:02.892Z"
  },
  "leadId": "69b2c41d352a78479d2c623b",
  "lead": { "_id": "69b2c41d352a78479d2c623b", "firstName": "BG", "lastName": "Real", "email": "goforbg@icloud.com", "phone": "+919042956129", "listId": "69ab444ac1feb77d7f46462f", "workspaceId": "org_3AZs4H8UsfFxVFONr6H4K75okaG", "source": "gohighlevel" },
  "campaignId": null,
  "campaignName": null,
  "ghlContactId": "aMUQn0u0Z7cw0NQ7tJ5R",
  "ghlLocationId": "eL7DD22BdZ0rismu7qCA",
  "hsPortalId": null,
  "hsContactId": null,
  "data": {}
}
event
string
"message.sent"
messageId
string
Tuco message ID
message
object
Full message document (not string)
leadId
string
Lead ID when known
lead
object
Full lead when available
ghlContactId
string | null
GHL contact ID for integrations
ghlLocationId
string | null
GHL location ID for integrations
timestamp
string
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

{
  "event": "message.failed",
  "timestamp": "2026-03-12T22:45:00.000Z",
  "workspaceId": "org_xxx",
  "messageId": "69b3414e6dae1d942ece4d0d",
  "message": {
    "_id": "69b3414e6dae1d942ece4d0d",
    "message": "Hi from N8N - mar 13",
    "messageType": "imessage",
    "status": "failed",
    "recipientPhone": "+919894486982",
    "recipientName": "Usha Giridhar",
    "leadId": "69b3414e6dae1d942ece4d0c",
    "workspaceId": "org_xxx",
    "errorMessage": "Prospect's iMessage not available on +919894486982",
    "createdAt": "2026-03-12T22:42:22.379Z",
    "updatedAt": "2026-03-12T22:42:22.962Z"
  },
  "leadId": "69b3414e6dae1d942ece4d0c",
  "lead": { "_id": "69b3414e6dae1d942ece4d0c", "firstName": "Usha", "lastName": "Giridhar", "phone": "+919894486982" },
  "campaignId": null,
  "campaignName": null,
  "ghlContactId": "8LUvKi8KL6oPnjHuUeAG",
  "ghlLocationId": "eL7DD22BdZ0rismu7qCA",
  "hsPortalId": null,
  "hsContactId": null,
  "data": { "error": "Prospect's iMessage not available on +919894486982" }
}
event
string
"message.failed"
message
object
Full message document (includes errorMessage)
data.error
string
Human-readable explanation of the failure
When sendFallbackSmsOnFailed is enabled (per-message or workspace-level) and Twilio is configured, Tuco automatically sends a fallback SMS on this event. You don’t need to build that logic yourself.

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

{
  "event": "message.fallback",
  "timestamp": "2026-03-12T22:42:23.000Z",
  "workspaceId": "org_xxx",
  "messageId": "69b3414e6dae1d942ece4d0d",
  "message": {
    "_id": "69b3414e6dae1d942ece4d0d",
    "message": "Hi from N8N - mar 13",
    "messageType": "imessage",
    "status": "fallback",
    "recipientPhone": "+919894486982",
    "recipientName": "Usha Giridhar",
    "leadId": "69b3414e6dae1d942ece4d0c",
    "workspaceId": "org_xxx",
    "errorMessage": "Prospect's iMessage not available on +919894486982",
    "createdAt": "2026-03-12T22:42:22.379Z",
    "updatedAt": "2026-03-12T22:42:22.962Z"
  },
  "leadId": "69b3414e6dae1d942ece4d0c",
  "lead": { "_id": "69b3414e6dae1d942ece4d0c", "firstName": "Usha", "lastName": "Giridhar", "phone": "+919894486982" },
  "campaignId": null,
  "campaignName": null,
  "ghlContactId": "8LUvKi8KL6oPnjHuUeAG",
  "ghlLocationId": "eL7DD22BdZ0rismu7qCA",
  "hsPortalId": null,
  "hsContactId": null,
  "data": {
    "reason": "Prospect's iMessage not available on +919894486982",
    "checkedAddresses": ["+919894486982"]
  }
}
event
string
"message.fallback"
message
object
Full message document (status fallback)
data.reason
string
Human-readable explanation
data.checkedAddresses
string[]
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

{
  "event": "message.opened",
  "timestamp": "2026-03-13T10:00:00.000Z",
  "workspaceId": "org_xxx",
  "messageId": "69b344bc6dae1d942ece4d0e",
  "message": {
    "_id": "69b344bc6dae1d942ece4d0e",
    "message": "Here's your document.",
    "messageType": "imessage",
    "status": "sent",
    "fromLineId": "69ab427ac1feb77d7f46462e",
    "recipientPhone": "+919042956129",
    "recipientEmail": "goforbg@icloud.com",
    "recipientName": "BG Real",
    "leadId": "69b2c41d352a78479d2c623b",
    "workspaceId": "org_xxx",
    "createdByUserId": "user_xxx",
    "sentAt": "2026-03-12T22:57:02.892Z",
    "readAt": "2026-03-13T10:00:00.000Z",
    "createdAt": "2026-03-12T22:57:00.856Z",
    "updatedAt": "2026-03-13T10:00:00.000Z"
  },
  "leadId": "69b2c41d352a78479d2c623b",
  "lead": { "_id": "69b2c41d352a78479d2c623b", "firstName": "BG", "lastName": "Real", "email": "goforbg@icloud.com", "phone": "+919042956129" },
  "campaignId": null,
  "campaignName": null,
  "ghlContactId": "aMUQn0u0Z7cw0NQ7tJ5R",
  "ghlLocationId": "eL7DD22BdZ0rismu7qCA",
  "hsPortalId": null,
  "hsContactId": null,
  "data": { "readAt": "2026-03-13T10:00:00.000Z" }
}
event
string
"message.opened"
message
object
Full message document including readAt (ISO UTC when read)
data.readAt
string
When the message was opened (ISO UTC)

Shared fields (all events)

Every webhook payload includes these base fields:
FieldDescription
eventEvent type: message.sent, message.failed, message.fallback, message.opened, or message.reply
timestampWhen the webhook was sent (ISO UTC)
workspaceIdYour workspace ID
messageIdTuco message ID — use as your primary key (for reply, this is the reply message ID)
messageFor sent/failed/fallback/opened: full message object. For reply: string (reply text only)
leadIdLead involved (when known)
leadFull lead object when available
ghlContactId, ghlLocationId, hsPortalId, hsContactIdIntegration IDs (nullable)
dataEvent-specific data (e.g. reason, checkedAddresses for fallback; reply, repliedAtUtc, parentMessages for reply; readAt for opened)

Consuming webhooks safely

1

Use messageId + event as your key

Make updates idempotent. If you’ve already processed a (messageId, event) pair, ignore duplicates.
2

Move status forward only

Only update your local status in the forward direction: queued → sent → delivered. Never move backwards.
3

Return 200 quickly

Tuco expects a 2xx response within a few seconds. Do heavy processing asynchronously after acknowledging the webhook.
4

Log workspaceId and leadId

These fields let you trace events back to the right tenant and contact in multi-workspace setups.
Tuco may retry webhook delivery if your endpoint returns a non-2xx status. Always make your handler idempotent.