Developer Portal
How-to guides

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 (from MESSAGE_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 MessageForward schema).
    • Exposed as :share_link_id in endpoints like:
      • GET /message-sharelinks/:share_link_id
      • PUT /message-sharelinks/:share_link_id/access
  • 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_id
    • GET /message-sharelinks/:share_link_id (with logged user)
    • POST /message-sharelinks
    • PUT /message-sharelinks/revoke-access
    • PUT /message-sharelinks/:share_link_id/access
    • POST /message-sharelinks/by-message-ids
  • Public endpoints (decorated with @Public()):
    • GET /v6/message-sharelinks/:share_link_id
    • GET /message-sharelinks/:share_link_id/attachments/signedurl/:attachment_id
    • GET /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 when share_type = "forward")
    • The message that will display the forward “bubble”.
  • **access_type** ("public" | "specified" | "forward", required)
  • **specified_access** (SpecifiedAccess[], required when access_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_id is not required for share_type = "link".
  • Anyone with the generated share_link_id can open it until end_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_id must be present.
    • access_type must be "forward".
    • specified_access must not be present.
    • message_id must 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:
    • 400 with code = "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_access must be a non-empty array.
  • When share_type = "link":
    • access_type cannot 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 — plus has_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_message does not expose a message_id field directly. The top-level message_id field is only present for share_type: "forward" shares (it holds the ID of the wrapper message created in the destination channel); it is absent for share_type: "link" shares. To get the original message's ID, read shared_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[].url already points at the share-link-scoped signed URL endpoint, which validates share-link access and works even when has_channel_access is 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
    GET /message-sharelinks/:share_link_id/attachments/signedurl/:attachment_id
    
    Example (public share, no auth header required)
    GET /message-sharelinks/share_1234567890abcdef/attachments/signedurl/att_1
    
    Example response
    "https://s3.amazonaws.com/bucket/file-name?X-Amz-SignedHeaders=..."
    
  • Audio file
    GET /message-sharelinks/:share_link_id/:audio_id
    
    This returns a 302 redirect to a signed S3 URL.
    GET /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 when access_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_type to "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 after start_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.
  • Valid share_type + access_type combinations
    • When share_type = "forward":
      • access_type must be "forward".
      • message_id must be provided.
      • specified_access must not be provided.
    • When share_type = "link":
      • access_type can be "public" or "specified".
      • specified_access is required if access_type = "specified".
  • 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.
  • Checking if a message has any public share
    • The service exposes messageHasPublicAccessShareLink(messageId) internally.
    • At the HTTP layer, use:
      • POST /message-sharelinks/by-message-ids and inspect is_public_shared.
  • Identifying the original message from a share
    • v6 (GET /v6/message-sharelinks/:share_link_id): shared_message.id is the original message ID. shared_message.conversation_id and shared_message.workspace_id are also directly available. Prefer v6 when you need the original message identity.
    • v2 (GET /message-sharelinks/:share_link_id): shared_message does not expose message_id directly. The top-level message_id is the forwarded wrapper message ID (only present for share_type: "forward"). To get the original message's ID, read shared_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.
  • Error handling
    • Treat 403 with codes like:
      • SHARED_MESSAGE_ACCESS_REVOKED
      • SHARED_MESSAGE_EXPIRED
      • FORWARDED_MESSAGE_ACCESS_REVOKED
      • FORWARDED_MESSAGE_EXPIRED
    • As hard denials: do not auto-retry with the same user context.

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
How Message Shares and Forwards Work | Carbon Voice Developer Portal