How Message Shares and Forwards Work
This document explains how Message Share Links and Forwards work in the CV API.
It is written for both humans and AI agents that need to:
- Create public share links or forwards
- Read shared messages and their attachments/audio
- Update who can access an existing share
- Revoke access to shares
- List all shares for one or many messages
All examples below assume the base path:
message-sharelinks(fromMESSAGE_SHARE_LINK_BASE_PATH)
Core Concepts
- Shared message (
shared_message_id)- The original message being exposed via a share/forward.
- Stored on the share as
shared_message_id.
- Forwarded/related message (
message_id)- When a message is forwarded, the target message that will show the “forward bubble”.
- Only used when
share_type = "forward". - Must be a message the logged-in user created.
- Share Link (
share_link_id)- The identifier of the share/forward itself (backed by
MessageForwardschema). - Exposed as
:share_link_idin endpoints like:GET /message-sharelinks/:share_link_idPUT /message-sharelinks/:share_link_id/access
- The identifier of the share/forward itself (backed by
- ShareType (
share_type)forward– create a forward inside a channel/message.link– create a share-by-link (public or specified access).
- AccessType (
access_type)public– anyone can open the share link (subject to expiry/revocation).forward– access linked to the underlying message/channel permissions.specified– only a set of users/channels/workspaces can open it.
- SpecifiedAccessType (
specified_access[].type)user– whitelist specific users (by id).channel– whitelist members of specific channels.workspace/workspace_group– whitelist users by workspace membership.
Internally, both forwards and share links are stored in the same model; behavior is driven by the combination of share_type and access_type.
Authentication and Base Path
- Authenticated endpoints (require
@TokenAuth()and@CurrentUser):GET /message-sharelinks/shared-links/:message_idGET /message-sharelinks/:share_link_id(with logged user)POST /message-sharelinksPUT /message-sharelinks/revoke-accessPUT /message-sharelinks/:share_link_id/accessPOST /message-sharelinks/by-message-ids
- Public endpoints (decorated with
@Public()):GET /v6/message-sharelinks/:share_link_idGET /message-sharelinks/:share_link_id/attachments/signedurl/:attachment_idGET /message-sharelinks/:share_link_id/:audio_id
AI agents should:
- Always include the user token for non-
@Public()endpoints. - Never assume access: respect
403/ custom error codes from the API.
1. Creating Shares and Forwards
Endpoint: POST /message-sharelinks
Body type: CreateMessageShareLink
Fields:
**shared_message_id** (string, required)- The message that is being shared/forwarded.
**share_type** ("link" | "forward", required)**message_id** (string, required only whenshare_type = "forward")- The message that will display the forward “bubble”.
**access_type** ("public" | "specified" | "forward", required)**specified_access**(SpecifiedAccess[], required whenaccess_type = "specified")**end_access_at**(Date, optional)- When the share link should expire.
1.1 Create a Public Share Link (anyone with link)
This creates a public share-by-link for a message.
Request
POST /message-sharelinks
Authorization: Bearer <token>
Content-Type: application/json
{
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "link",
"access_type": "public",
}
Key rules
message_idis not required forshare_type = "link".- Anyone with the generated
share_link_idcan open it untilend_access_at(or revocation).
Response (shape example)
{
"_id": "share_1234567890abcdef",
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "link",
"access_type": "public",
"created_by": "user_abc",
"created_at": "2026-03-10T10:00:00.000Z"
}
1.2 Create a Forward (share_type = forward)
This creates a forward of a message into another message/channel.
Request
POST /message-sharelinks
Authorization: Bearer <token>
Content-Type: application/json
{
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "forward",
"message_id": "65f1234567890abcdef0999",
"access_type": "forward"
}
Validation rules (service-level):
- When
share_type = "forward":message_idmust be present.access_typemust be"forward".specified_accessmust not be present.message_idmust be a message created by the logged-in user.
- If the user has already forwarded the same (
message_id,shared_message_id) combination, the API returns:400withcode = "MESSAGE_ALREADY_BEEN_SHARED".
1.3 Create a Link with Specified Access
This creates a share-by-link that is only visible to a limited audience.
Request
POST /message-sharelinks
Authorization: Bearer <token>
Content-Type: application/json
{
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "link",
"access_type": "specified",
"specified_access": [
{
"type": "user",
"ids": ["user_111", "user_222"]
},
{
"type": "channel",
"ids": ["channel_abc"]
}
],
"end_access_at": "2026-04-01T00:00:00.000Z"
}
Validation rules
- When
access_type = "specified":specified_accessmust be a non-empty array.
- When
share_type = "link":access_typecannot be"forward".
2. Reading Shares and Shared Content
2.1 Get a Share Link and Shared Message
There are two versions of the main read endpoint:
- v2 (default): returns
MessageShareLinkWithSharedMessage(rewritten URLs, limited info). - v6: returns
MessageShareLinkWithMessageV6— the original message in the MessageV6 shape — plushas_channel_access.
v2 – Get Share Link with Shared Message
GET /message-sharelinks/:share_link_id
Authorization: Bearer <token> # optional for public shares
Example response (simplified)
{
"_id": "share_1234567890abcdef",
"share_type": "link",
"access_type": "public",
"shared_message": {
"name": "Alice",
"is_text_message": false,
"duration_ms": 4200,
"creator_id": "user_abc",
"audio_models": [
{
"_id": "audio_1",
"url": "https://api.carbonvoice.app/message-sharelinks/share_1234567890abcdef/audio_1"
}
],
"text_models": [],
"attachments": [
{
"_id": "att_1",
"type": "file",
"link": "https://api.carbonvoice.app/message-sharelinks/share_1234567890abcdef/attachments/signedurl/att_1"
}
],
"original_message_info": {
"message_id": "65f1234567890abcdef0001",
"channel_id": "channel_abc",
"workspace_id": "workspace_xyz"
},
"created_at": 1700000000000,
"last_updated_at": 1700000000000
}
}
Note:
shared_messagedoes not expose amessage_idfield directly. The top-levelmessage_idfield is only present forshare_type: "forward"shares (it holds the ID of the wrapper message created in the destination channel); it is absent forshare_type: "link"shares. To get the original message's ID, readshared_message.original_message_info.message_id— but this field is only present when the authenticated user has access to the original message's channel (or is the message creator). Always guard against it being absent.
v6 – Get Share Link with Full Message
GET /v6/message-sharelinks/:share_link_id
Authorization: Bearer <token>
shared_message is the original message in the MessageV6 shape, the same shape the /v6/messages endpoints return.
Example response (simplified)
{
"_id": "share_1234567890abcdef",
"share_type": "link",
"access_type": "public",
"shared_message": {
"id": "65f1234567890abcdef0001",
"creator_id": "user_abc",
"conversation_id": "channel_abc",
"workspace_id": "workspace_xyz",
"content": {
"transcript": "Hey team, quick update on the launch",
"language": "en",
"url": "https://api.carbonvoice.app/stream/65f1234567890abcdef0001/audio_1/audio.mp3",
"duration_ms": 4200
},
"attachments": [
{
"id": "att_1",
"type": "file",
"url": "https://api.carbonvoice.app/message-sharelinks/share_1234567890abcdef/attachments/signedurl/att_1",
"filename": "notes.pdf"
}
],
"created_at": "2023-11-14T22:13:20.000Z",
"updated_at": "2023-11-14T22:13:20.000Z"
},
"has_channel_access": true
}
Attachment downloads (v6): for
type: "file"attachments,shared_message.attachments[].urlalready points at the share-link-scoped signed URL endpoint, which validates share-link access and works even whenhas_channel_accessis false:GET /message-sharelinks/{share_link_id}/attachments/signedurl/{attachment_id}Do not use
GET /v5/attachments/download/signedurl/{attachment_id}— that endpoint checks channel membership on the original message and will 403 when the agent lacks channel access.
2.2 Get All Share Links for a Shared Message
Endpoint: GET /message-sharelinks/shared-links/:message_id
Body type: GetMessageShareLinksBySharedMessageFilter (via query)
GET /message-sharelinks/shared-links/65f1234567890abcdef0001
Authorization: Bearer <token>
Example response (simplified)
{
"filters": {
"page": 1,
"limit": 20
},
"total_results": 2,
"results": [
{
"_id": "share_public_link",
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "link",
"access_type": "public",
"end_access_at": "2026-04-01T00:00:00.000Z"
},
{
"_id": "share_forward_channel",
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "forward",
"access_type": "forward",
"channel_id": "channel_abc"
}
]
}
The service applies visibility rules so the caller only sees forwards they’re allowed to know about.
2.3 Download Attachments and Audio from a Share
- Attachment signed URL
Example (public share, no auth header required)GET /message-sharelinks/:share_link_id/attachments/signedurl/:attachment_id
Example responseGET /message-sharelinks/share_1234567890abcdef/attachments/signedurl/att_1"https://s3.amazonaws.com/bucket/file-name?X-Amz-SignedHeaders=..." - Audio file
This returns a 302 redirect to a signed S3 URL.GET /message-sharelinks/:share_link_id/:audio_idGET /message-sharelinks/share_1234567890abcdef/audio_1
3. Revoking Access to Shares
Endpoint: PUT /message-sharelinks/revoke-access
Body type: RevokeAccessToShareLink
You can:
- Revoke one specific share link (
share_link_id), or - Revoke all share links that point to a given
shared_message_id.
Exactly one of share_link_id or shared_message_id must be provided.
3.1 Revoke a Single Share Link
Request
PUT /message-sharelinks/revoke-access
Authorization: Bearer <token>
Content-Type: application/json
{
"share_link_id": "share_1234567890abcdef"
}
Permissions
- Allowed if:
- Logged user is the creator of the share link; or
- Logged user is the creator of the original shared message.
On success, the API returns 204 No Content.
3.2 Revoke All Share Links for a Shared Message
Request
PUT /message-sharelinks/revoke-access
Authorization: Bearer <token>
Content-Type: application/json
{
"shared_message_id": "65f1234567890abcdef0001"
}
This revokes all shares (both forwards and links) that reference this shared_message_id.
The service also notifies clients and updates related messages’ last_updated_at where applicable.
4. Updating Access for an Existing Share Link
Endpoint: PUT /message-sharelinks/:share_link_id/access
Body type: UpdateAccessPayload
This only works when:
share_type = "link"for the target share.- Logged user is the creator of the share link.
UpdateAccessPayload fields:
**access_type** ("public" | "specified")**specified_access** (SpecifiedAccess[], optional; required whenaccess_type = "specified")**end_access_at** (Date, optional)
4.1 Make a Share Link Public
Request
PUT /message-sharelinks/share_1234567890abcdef/access
Authorization: Bearer <token>
Content-Type: application/json
{
"access_type": "public",
"end_access_at": "2026-05-01T00:00:00.000Z"
}
Effect
- Sets
access_typeto"public"and updates the expiration date.
4.2 Restrict a Share Link to Specific Users
Request
PUT /message-sharelinks/share_1234567890abcdef/access
Authorization: Bearer <token>
Content-Type: application/json
{
"access_type": "specified",
"specified_access": [
{
"type": "user",
"ids": ["user_111", "user_222"]
}
],
"end_access_at": "2026-05-01T00:00:00.000Z"
}
If access_type = "specified" and specified_access is empty, the service throws 400 Bad Request.
5. Listing Share Links by Message IDs (Creator View)
Endpoint: POST /message-sharelinks/by-message-ids
Body type: GetListOfSharelinksByMessageIds
This endpoint lets the creator of messages see, for many messages at once:
- Whether each message is public-shared.
- Which share links exist per message.
Request body
**message_ids** (string[], required, 1–500)**start_date** (Date, optional)**end_date** (Date, optional, must be afterstart_date, with a max allowed range)
5.1 Example: Get Shares for Two Messages
Request
POST /message-sharelinks/by-message-ids
Authorization: Bearer <token>
Content-Type: application/json
{
"message_ids": [
"65f1234567890abcdef0001",
"65f1234567890abcdef0002"
],
"start_date": "2026-03-01T00:00:00.000Z",
"end_date": "2026-03-31T23:59:59.000Z"
}
Example response
[
{
"message_id": "65f1234567890abcdef0001",
"is_public_shared": true,
"sharelinks": [
{
"_id": "share_public_link_1",
"shared_message_id": "65f1234567890abcdef0001",
"share_type": "link",
"access_type": "public",
"end_access_at": "2026-04-01T00:00:00.000Z"
}
]
},
{
"message_id": "65f1234567890abcdef0002",
"is_public_shared": false,
"sharelinks": []
}
]
Notes:
- Only messages where the logged user is the creator are considered when returning share links.
- Messages for which the user is not the creator will appear with
sharelinks: [].
6. Behavioral Rules for AI Agents
- Choosing
share_type- Use
share_type = "link"for share-by-link flows (public or specified). - Use
share_type = "forward"when creating a forward bubble attached to another message.
- Use
- Valid
share_type+access_typecombinations- When
share_type = "forward":access_typemust be"forward".message_idmust be provided.specified_accessmust not be provided.
- When
share_type = "link":access_typecan be"public"or"specified".specified_accessis required ifaccess_type = "specified".
- When
- Revocation behavior
- Prefer targeted revocation (
share_link_id) if you know the exact share to revoke. - Use bulk revocation (
shared_message_id) to remove all access to a message in one go.
- Prefer targeted revocation (
- Checking if a message has any public share
- The service exposes
messageHasPublicAccessShareLink(messageId)internally. - At the HTTP layer, use:
POST /message-sharelinks/by-message-idsand inspectis_public_shared.
- The service exposes
- Identifying the original message from a share
- v6 (
GET /v6/message-sharelinks/:share_link_id):shared_message.idis the original message ID.shared_message.conversation_idandshared_message.workspace_idare also directly available. Prefer v6 when you need the original message identity. - v2 (
GET /message-sharelinks/:share_link_id):shared_messagedoes not exposemessage_iddirectly. The top-levelmessage_idis the forwarded wrapper message ID (only present forshare_type: "forward"). To get the original message's ID, readshared_message.original_message_info.message_id— but this field is conditional: it is only present when the authenticated user has access to the original channel or is the message creator. Always guard against it being absent.
- v6 (
- Error handling
- Treat
403with codes like:SHARED_MESSAGE_ACCESS_REVOKEDSHARED_MESSAGE_EXPIREDFORWARDED_MESSAGE_ACCESS_REVOKEDFORWARDED_MESSAGE_EXPIRED
- As hard denials: do not auto-retry with the same user context.
- Treat
7. Quick Endpoint Reference
- Create share / forward
POST /message-sharelinks
- Read share (v2)
GET /message-sharelinks/:share_link_id
- Read share (v6 — exposes the original message as MessageV6:
id,conversation_id,workspace_id)GET /v6/message-sharelinks/:share_link_id
- List share links for one shared message
GET /message-sharelinks/shared-links/:message_id
- Get attachment signed URL
GET /message-sharelinks/:share_link_id/attachments/signedurl/:attachment_id
- Get audio file (redirect)
GET /message-sharelinks/:share_link_id/:audio_id
- Revoke access
PUT /message-sharelinks/revoke-access
- Update access for a link
PUT /message-sharelinks/:share_link_id/access
- List shares by message IDs (creator view)
POST /message-sharelinks/by-message-ids