# 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:

```json
{
  "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 /v3/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 channel |
| `shared_message` | The original message content (see below) |

**`shared_message` fields:**

| Field | Description |
| --- | --- |
| `message_id` | ID of the original message |
| `creator_id` | Who originally sent the message |
| `channel_ids` | Array of channel IDs the original message belongs to |
| `workspace_ids` | Array of workspace IDs the original message belongs to |
| `text_models` | Array of text/transcript objects. Entries with `type: "transcript"` carry the text in `value`. Entries with `type: "transcript_with_timecode"` have `value: ""` — the words are in `timecodes[].word` and must be joined to reconstruct the transcript. |
| `audio_models` | Array of audio objects with playback URLs — use `url` directly |
| `attachments` | Attachments from the original message — `link` is an internal storage path; exchange `_id` via `GET /message-sharelinks/{share_link_id}/attachments/signedurl/{attachment_id}` before downloading |
| `duration_ms` | Duration of the original voice message |
| `created_at` | When the original message was sent |

> **To get the original message ID**, read `shared_message.message_id` directly.

## Access Attachments and Audio

Audio URLs in `shared_message.audio_models[].url` are normal playback URLs and can be used directly.

File attachment `link` values in `shared_message.attachments` are internal storage paths and are **not** directly accessible. Use the share-link-scoped signed URL endpoint, which validates share-link access rather than requiring channel 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 channel access on the original message and will 403 when `has_channel_access` is false.

## Full Example — Webhook Handler

```javascript
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/v3/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.message_id;

  // transcript_with_timecode entries have value:'' — words are in timecodes[].word
  const transcript = (() => {
    const plain = shared_message.text_models?.find(
      (m) => m.type === 'transcript' && m.value,
    );
    if (plain) return plain.value;
    const timed = shared_message.text_models?.find(
      (m) => m.type === 'transcript_with_timecode' && m.timecodes?.length,
    );
    if (timed) return timed.timecodes.map((t) => t.word).join(' ');
    return '';
  })();

  // reply with a summary or acknowledgement
  await fetch('https://api.carbonvoice.app/v5/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,
    }),
  });
}
```

## Related

- [How to Handle Incoming Webhook Payloads](./how-to-handle-incoming-webhook-payloads.md)
- [How to Send an Existing Message](./how-to-send-an-existing-message.md)
- [How to Download a File Attachment](./how-to-download-a-file-attachment.md)
