Summary
Contacts that originate from a Facebook comment cannot be messaged through the public API — even after the contact replies to the private reply and a standard 24-hour messaging window is open. The send returns 204, a message row is created, and nothing is delivered. sourceId is never populated on the row, which is the only way to tell from outside that it failed.
Sends to non-comment-origin contacts on the same inbox work reliably (22/22 in our sample), so this is specific to comment-originated contacts.
Cloud (app.chatbotx.io), Messenger channel, workspace API key.
Reproduction
Contact A (11630996004388864), inbox 11627001976487936. Times UTC, same thread:
| time |
messageType |
senderType |
delivered? |
note |
| 10:47:30 |
outgoing |
user |
yes (sourceId set) |
sent from Meta Business Suite, echoed in |
| 10:47:50 |
incoming |
contact |
yes |
contact replies — 24h window opens |
| 10:47:59 |
outgoing |
api |
no (sourceId null) |
POST /v1/contacts/id:.../messages → 204 |
Contact B (11630484688945152) is identical: contact sends "Is this free?" at 01:48:02, our API reply at 01:48:11 never delivers.
Sending manually from the ChatbotX inbox UI on the message thread also fails to deliver, so this is not specific to the public API.
Both contacts have a normal 17-digit contactInbox.sourceId, the same shape as contacts we message successfully. Their conversation.sourceId is <pageId>_<postId>.
Expected
Per Meta's Private Replies docs:
Only when a person responds to the private message can you continue the conversation within the 24-hour messaging window.
The contact responded, so a normal send should succeed.
Suspected cause
apps/worker/src/integration/handlers/received-message.ts — receiveComment keys the conversation to the post:
const incomingContact: IncomingContact = {
sourceId: commentData.fromId,
sourceConversationId: commentData.postId,
}
apps/worker/src/chat/handlers/send-message.ts then forwards that to the channel handler:
contact: { ...contactInbox, sourceConversationId: conversation.sourceId }
So a send resolving the comment conversation hands the channel a post id where a working contact supplies a message thread id. (The messenger channel implementation isn't in this repo, so I couldn't verify past this point.)
Possibly compounded by the assumption on the line above it:
// `from.id` is the commenter's ID (PSID for Messenger, ...)
Meta treats IDs across surfaces as not interchangeable — hence the ID Matching API. If a comment's from.id is not the messaging PSID for that Page, storing it as contactInbox.sourceId would produce exactly this: a well-formed id the Send API rejects.
Secondary issue: the failure is invisible to API consumers
send-message.ts emits message:failed internally, but nothing is persisted on the message row and the public API exposes no delivery status. From outside, a dead message is indistinguishable from a delivered one except by inferring from sourceId: null. This is the same silent-failure shape described in #831.
Surfacing a delivery status on the message resource would let integrators detect this without guesswork.
Third issue: lastIncomingMessageAt ignores comments and attachment-only messages
received-message.ts:
if (incomingMessage.messageType !== "outgoing" &&
(incomingMessage.type ?? "message") === "message") {
tracking.lastIncomingMessageAt = message.createdAt
Comments never update it, and attachment-only messages appear not to either. Since the unreplied and lastInteractionMinutesAgo contact filters are built on that column, any integrator polling for "who is waiting on a reply" is blind to both. In our workspace 48/50 comment-origin contacts have lastIncomingMessageAt: null.
Impact
Comment-to-DM is our main lead source. Every one of those leads who engages gets a reply that is recorded, billed, and shown as sent — and never arrives. It is silent on every surface except the Facebook thread itself.
Happy to test any fix against a live workspace.
Summary
Contacts that originate from a Facebook comment cannot be messaged through the public API — even after the contact replies to the private reply and a standard 24-hour messaging window is open. The send returns
204, a message row is created, and nothing is delivered.sourceIdis never populated on the row, which is the only way to tell from outside that it failed.Sends to non-comment-origin contacts on the same inbox work reliably (22/22 in our sample), so this is specific to comment-originated contacts.
Cloud (app.chatbotx.io), Messenger channel, workspace API key.
Reproduction
Contact A (
11630996004388864), inbox11627001976487936. Times UTC, same thread:usersourceIdset)contactapisourceIdnull)POST /v1/contacts/id:.../messages→ 204Contact B (
11630484688945152) is identical: contact sends"Is this free?"at 01:48:02, our API reply at 01:48:11 never delivers.Sending manually from the ChatbotX inbox UI on the message thread also fails to deliver, so this is not specific to the public API.
Both contacts have a normal 17-digit
contactInbox.sourceId, the same shape as contacts we message successfully. Theirconversation.sourceIdis<pageId>_<postId>.Expected
Per Meta's Private Replies docs:
The contact responded, so a normal send should succeed.
Suspected cause
apps/worker/src/integration/handlers/received-message.ts—receiveCommentkeys the conversation to the post:apps/worker/src/chat/handlers/send-message.tsthen forwards that to the channel handler:So a send resolving the comment conversation hands the channel a post id where a working contact supplies a message thread id. (The messenger channel implementation isn't in this repo, so I couldn't verify past this point.)
Possibly compounded by the assumption on the line above it:
// `from.id` is the commenter's ID (PSID for Messenger, ...)Meta treats IDs across surfaces as not interchangeable — hence the ID Matching API. If a comment's
from.idis not the messaging PSID for that Page, storing it ascontactInbox.sourceIdwould produce exactly this: a well-formed id the Send API rejects.Secondary issue: the failure is invisible to API consumers
send-message.tsemitsmessage:failedinternally, but nothing is persisted on the message row and the public API exposes no delivery status. From outside, a dead message is indistinguishable from a delivered one except by inferring fromsourceId: null. This is the same silent-failure shape described in #831.Surfacing a delivery status on the message resource would let integrators detect this without guesswork.
Third issue:
lastIncomingMessageAtignores comments and attachment-only messagesreceived-message.ts:Comments never update it, and attachment-only messages appear not to either. Since the
unrepliedandlastInteractionMinutesAgocontact filters are built on that column, any integrator polling for "who is waiting on a reply" is blind to both. In our workspace 48/50 comment-origin contacts havelastIncomingMessageAt: null.Impact
Comment-to-DM is our main lead source. Every one of those leads who engages gets a reply that is recorded, billed, and shown as sent — and never arrives. It is silent on every surface except the Facebook thread itself.
Happy to test any fix against a live workspace.