How to Access a Forwarded Message
When a user forwards an existing message into a conversation, the incoming webhook contains a share_link_id field. Use that ID to fetch the original message content — transcript, audio, and attachments — via the share link endpoint.
Detecting a Forwarded Message
A forwarded message arrives as a normal message.posted.to.channel webhook event. Check for a non-null share_link_id in the payload to identify it:
{
"event": "message.posted.to.channel",
"data": {
"id": "new-message-uuid",
"conversation_id": "channel-uuid",
"share_link_id": "share-link-uuid",
"transcript_txt": "Check this out",
"creator_id": "user-who-forwarded-uuid"
}
}
share_link_id being non-null is the only signal you need — transcript_txt may also be present if the sender added accompanying text, but the original forwarded content lives behind the share link.
Fetch the Original Message Content
GET /v6/message-sharelinks/{share_link_id}
Authorization: Bearer <your-pat>
Response — key fields:
| Field | Description |
|---|---|
share_type | "forward" for forwarded messages, "link" for public share links |
created_by | User who performed the forward |
end_access_at | Expiration timestamp (null if no expiry) |
revoked_at | Set if access has been revoked |
has_channel_access | Whether the authenticated user has access to the original message's conversation |
shared_message | The original message, in the same MessageV6 shape the /v6/messages endpoints return (see below) |
shared_message fields:
| Field | Description |
|---|---|
id | ID of the original message |
creator_id | Who originally sent the message |
conversation_id | The conversation the original message belongs to |
workspace_id | The workspace the original message belongs to |
content.transcript | The transcript of the original message as plain text |
content.ai_summary | AI summary of the original message, when one exists |
content.time_codes | Word-level time codes, when available |
content.url | Playback URL of the original audio |
content.duration_ms | Duration of the original voice message |
attachments | Attachments from the original message. For type: "file", url already points at GET /message-sharelinks/{share_link_id}/attachments/signedurl/{id}, which returns the download URL |
created_at | When the original message was sent |
To get the original message ID, read
shared_message.iddirectly.
Access Attachments and Audio
Play the original audio from shared_message.content.url.
File attachments are stored privately and are not directly downloadable. The url of each type: "file" entry in shared_message.attachments is the share-link-scoped signed URL endpoint, which validates share-link access rather than requiring conversation membership:
GET /message-sharelinks/{share_link_id}/attachments/signedurl/{attachment_id}
Authorization: Bearer <your-pat>
The response is a signed URL you can use to download the file. Do not use GET /v5/attachments/download/signedurl/{attachment_id} here — that endpoint checks access to the original message's conversation and will 403 when has_channel_access is false.
Full Example — Webhook Handler
async function handleWebhook(webhook) {
const { share_link_id, conversation_id, id } = webhook.data;
if (!share_link_id) return; // not a forwarded message
const res = await fetch(
`https://api.carbonvoice.app/v6/message-sharelinks/${share_link_id}`,
{
headers: {
Authorization: `Bearer ${process.env.CV_PAT}`,
},
},
);
const shareLink = await res.json();
if (shareLink.revoked_at) return; // access revoked
const { shared_message } = shareLink;
const originalMessageId = shared_message.id;
const transcript = shared_message.content?.transcript ?? '';
// reply with a summary or acknowledgement
await fetch('https://api.carbonvoice.app/v6/messages/text', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CV_PAT}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversation_id,
transcript: `Got the forwarded message: "${transcript}"`,
reply_to_message_id: id,
}),
});
}