diff --git a/.gitignore b/.gitignore
index b1ba4f8b95..11c7f8ca85 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@ config/nginx-redirects.conf
src/gatsby-types.d.ts
.idea/*
**/*.swp
+.claude
diff --git a/content/api/realtime-sdk/messages.textile b/content/api/realtime-sdk/messages.textile
index e9a092bc1c..c4519ed736 100644
--- a/content/api/realtime-sdk/messages.textile
+++ b/content/api/realtime-sdk/messages.textile
@@ -25,3 +25,8 @@ h2(#properties).
python: Attributes
<%= partial partial_version('types/_message') %>
+
+h2(#message-annotations).
+ default: MessageAnnotations
+
+<%= partial partial_version('types/_message_annotations') %>
diff --git a/content/api/realtime-sdk/types.textile b/content/api/realtime-sdk/types.textile
index 71e3bcde68..b3f817d4d5 100644
--- a/content/api/realtime-sdk/types.textile
+++ b/content/api/realtime-sdk/types.textile
@@ -87,10 +87,10 @@ blang[jsall].
<%= partial partial_version('types/_message_action') %>
- h3(#message-operation).
- default: Operation
+h3(#message-annotations).
+ default: MessageAnnotations
- <%= partial partial_version('types/_operation') %>
+<%= partial partial_version('types/_message_annotations') %>
h3(#presence-message).
default: PresenceMessage
diff --git a/content/partials/core-features/_authentication_capabilities.textile b/content/partials/core-features/_authentication_capabilities.textile
index 51ef064fb0..10f21c78d3 100644
--- a/content/partials/core-features/_authentication_capabilities.textile
+++ b/content/partials/core-features/_authentication_capabilities.textile
@@ -5,6 +5,8 @@ The following capability operations are available for API keys and issued tokens
- presence := can register presence on a channel (enter, update and leave)
- object-subscribe := can subscribe to updates to objects on a channel
- object-publish := can update objects on a channel
+- annotation-subscribe := can subscribe to individual annotations on a channel
+- annotation-publish := can publish annotations to messages on a channel
- history := can retrieve message and presence state history on channels
- stats := can retrieve current and historical usage statistics for an app
- push-subscribe := can subscribe devices for push notifications
diff --git a/content/partials/types/_message.textile b/content/partials/types/_message.textile
index b9532413f4..d392589dea 100644
--- a/content/partials/types/_message.textile
+++ b/content/partials/types/_message.textile
@@ -51,7 +51,7 @@ h6(#timestamp).
default: timestamp
csharp: Timestamp
-Timestamp when the message was received by the Ably, as milliseconds since the epocha @Time@ object .__Type: @Integer@@Long Integer@@DateTimeOffset@@Time@@NSDate@__
+Timestamp when the message was first received by the Ably, as milliseconds since the epocha @Time@ object .__Type: @Integer@@Long Integer@@DateTimeOffset@@Time@@NSDate@__
h6(#encoding).
default: encoding
@@ -69,22 +69,12 @@ blang[jsall].
h6(#serial).
default: serial
- This message's unique serial (an identifier that will be the same in all future updates of this message). __Type: @String@__
+ A server-assigned identifier that will be the same in all future updates of this message. It can be used to add annotations to a message. Serial will only be set if you enable annotations in "channel rules":/docs/channels#rules . __Type: @String@__
- h6(#created-at).
- default: createdAt
+ h6(#annotations).
+ default: annotations
- The timestamp of the very first version of a given message (will differ from @timestamp@ only if the message has been updated or deleted). __Type: @Integer@__
-
- h6(#version).
- default: version
-
- The version of the message, lexicographically-comparable with other versions (that share the same serial). Will differ from the serial only if the message has been updated or deleted. __Type: @String@__
-
- h6(#operation).
- default: operation
-
- In the case of an updated or deleted message, this will contain metadata about the update or delete operation. __Type: "@Operation@":/docs/api/realtime-sdk/types#message-operation__
+ An object containing information about annotations that have been made to the object. __Type: "@MessageAnnotations@":/docs/api/realtime-sdk/types#message-annotations__
h3(constructors).
default: Message constructors
diff --git a/content/partials/types/_message_annotations.textile b/content/partials/types/_message_annotations.textile
new file mode 100644
index 0000000000..69fad98670
--- /dev/null
+++ b/content/partials/types/_message_annotations.textile
@@ -0,0 +1,7 @@
+h4.
+ default: Properties
+ java: Members
+ ruby: Attributes
+ python: Attributes
+
+- summary := An object whose keys are annotation types, and the values are aggregated summaries for that annotation type __Type: @Record@__
diff --git a/content/partials/types/_operation.textile b/content/partials/types/_operation.textile
deleted file mode 100644
index 02e0e9d02f..0000000000
--- a/content/partials/types/_operation.textile
+++ /dev/null
@@ -1,10 +0,0 @@
-An @Operation@ contains the details of an operation, such as update or deletion, supplied by the actioning client.
-
-h4.
- default: Properties
-
-- clientId := The client ID of the client that initiated the operation __Type: @String@__
-
-- description := The description provided by the client that initiated the operation __Type: @String@__
-
-- metadata := A JSON object of string key-value pairs that may contain metadata associated with the operation __Type: @Record@__
diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts
index efd127b762..3d949f045d 100644
--- a/src/data/nav/pubsub.ts
+++ b/src/data/nav/pubsub.ts
@@ -195,6 +195,10 @@ export default {
name: 'Message batching',
link: '/docs/messages/batch',
},
+ {
+ name: 'Message annotations',
+ link: '/docs/messages/annotations',
+ },
],
},
{
diff --git a/src/pages/docs/auth/capabilities.mdx b/src/pages/docs/auth/capabilities.mdx
index fd25089dc0..1551dac175 100644
--- a/src/pages/docs/auth/capabilities.mdx
+++ b/src/pages/docs/auth/capabilities.mdx
@@ -40,6 +40,8 @@ The following capability operations are available for API keys and issued tokens
| **presence** | Can register presence on a channel (enter, update and leave) |
| **object-subscribe** | Can subscribe to updates to objects on a channel |
| **object-publish** | Can update objects on a channel |
+| **annotation-subscribe** | Can subscribe to individual annotations on a channel |
+| **annotation-publish** | Can publish annotations to messages on a channel |
| **history** | Can retrieve message and presence state history on channels |
| **stats** | Can retrieve current and historical usage statistics for an app |
| **push-subscribe** | Can subscribe devices for push notifications |
diff --git a/src/pages/docs/channels/index.mdx b/src/pages/docs/channels/index.mdx
index d326c89279..3bcdd9d03f 100644
--- a/src/pages/docs/channels/index.mdx
+++ b/src/pages/docs/channels/index.mdx
@@ -200,6 +200,7 @@ The channel rules related to enabling features are:
| Push notifications enabled | If checked, publishing messages with a push payload in the `extras` field is permitted. This triggers the delivery of a [Push Notification](/docs/push) to devices registered for push on the channel. |
| Server-side batching | If enabled, messages are grouped into batches before being sent to subscribers. [Server-side batching](/docs/messages/batch#server-side) reduces the overall message count, lowers costs, and mitigates the risk of hitting rate limits during high-throughput scenarios. |
| Message conflation | If enabled, messages are aggregated over a set period of time and evaluated against a conflation key. All but the latest message for each conflation key value will be discarded, and the resulting message, or messages, will be delivered to subscribers as a single batch once the period of time elapses. [Message conflation](/docs/messages#conflation) reduces costs in high-throughput scenarios by removing redundant and outdated messages. |
+| Message annotations, updates, and deletes | If enabled, allows message "annotations":/docs/messages/annotations to be used, as well as updates and deletes to be published to messages. Note that these features are currently Experimental, still in development, and subject to change. When this feature is enabled, messages will be "persisted":/docs/storage-history/storage#all-message-persistence (necessary in order from them later be annotated or updated), and "continuous history":/docs/storage-history/history#continuous-history features will not work.
To set a channel rule in the Ably dashboard:
diff --git a/src/pages/docs/channels/options/index.mdx b/src/pages/docs/channels/options/index.mdx
index ec53601110..7f08fe1c02 100644
--- a/src/pages/docs/channels/options/index.mdx
+++ b/src/pages/docs/channels/options/index.mdx
@@ -438,6 +438,8 @@ The available set of channel mode flags are:
| `PRESENCE` | Can register presence on the channel. | Yes |
| `OBJECT_PUBLISH` | Can update objects on the channel. | No |
| `OBJECT_SUBSCRIBE` | Can subscribe to receive updates to objects on the channel. | No |
+| `ANNOTATION_PUBLISH` | Can publish annotations to messages on the channel. | Yes |
+| `ANNOTATION_SUBSCRIBE` | Can subscribe to individual annotations on the channel. | No |
The set of modes available to a client is determined by the set of [capabilities](/docs/auth/capabilities) granted by their token or API key.
@@ -450,6 +452,8 @@ The modes granted by each capability are:
| `presence` | `PRESENCE` |
| `object-subscribe` | `OBJECT_SUBSCRIBE` |
| `object-publish` | `OBJECT_PUBLISH` |
+| `annotation-publish` | `ANNOTATION_PUBLISH` |
+| `annotation-subscribe` | `ANNOTATION_SUBSCRIBE` |
The actual modes assigned to a client will be the **intersection** of the requested `modes` and the modes available to the client according to its capabilities. For example, a client with the `subscribe` capability which explicitly requests `SUBSCRIBE` and `PUBLISH` modes will be assigned only the `SUBSCRIBE` mode.
diff --git a/src/pages/docs/messages/annotations.mdx b/src/pages/docs/messages/annotations.mdx
new file mode 100644
index 0000000000..4afa87dfe7
--- /dev/null
+++ b/src/pages/docs/messages/annotations.mdx
@@ -0,0 +1,494 @@
+---
+title: Message annotations
+meta_description: "Annotate messages on a channel with additional metadata."
+---
+
+
+
+Message annotations enable clients to add metadata to existing messages on a channel. You can use annotations to implement features like:
+
+* **Message reactions** - add emoji reactions (👍, ❤️, 😂) to messages
+* **Content categorization** - tag messages with categories such as "important" or "urgent"
+* **Community moderation** - flag inappropriate content for review
+* **Read receipts** - mark messages as "read" or "delivered"
+
+When clients publish or delete an annotation, Ably automatically creates a [summary](#annotation-summaries) that provides an aggregated view of all annotations for that message.
+
+## Enable annotations
+
+Annotations can be enabled for a channel or channel namespace with the *Message annotations, updates, and deletes* channel rule.
+
+
+
+1. Go to the [**Settings**](https://ably.com/accounts/any/apps/any/edit) tab of an app in your dashboard.
+3. Under [channel rules](/docs/channels#rules), click **Add new rule**.
+4. Enter the channel name or channel namespace on which to enable message annotations.
+5. Check **Message annotations, updates, and deletes** to enable message annotations.
+6. Click **Create channel rule** to save.
+
+## Annotation types
+
+Annotation types determine how annotations are processed and aggregated into [summaries](#annotation-summaries).
+
+The annotation type is a string of the format `namespace:summarization.version` where:
+
+* `namespace` is a string (e.g. `reactions`) that groups related annotations. Only annotations in the same namespace will be aggregated together to produce [summaries](#annotation-summaries).
+* `summarization` specifies how annotations are aggregated to produce [summaries](#annotation-summaries), such as `total`, `flag`, `distinct`, `unique`, or `multiple`.
+* `version` specifies the version component which allows for future changes to summarization behavior.
+
+### Total
+
+The `total.v1` summarization method counts the number of annotations of a given type that were published for a message.
+
+Deleting an annotation decrements the total count for that message.
+
+Using `total.v1` does not attribute counts to individual clients in the summary; it maintains only a simple count per annotation type and does not organize counts by name. [Unidentified](/docs/auth/identified-clients#unidentified) clients can publish `total.v1` annotations. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations.
+
+If the same client publishes an annotation of a given type to the same message twice, the `total` count is incremented twice.
+
+
+```json
+{
+ "metrics:total.v1": {
+ "total": 42
+ }
+}
+```
+
+
+### Flag
+
+The `flag.v1` summarization method counts how many distinct clients have published an annotation of a given type and maintains a list of those `clientId`s. Clients must be [identified](/docs/auth/identified-clients) to publish `flag.v1` annotations.
+
+Deleting an annotation decrements the total count for that message and removes the `clientId` from the list of clients that contributed to the summary.
+
+A given client can contribute to the summary only once per annotation type.
+
+
+```json
+{
+ "reactions:flag.v1": {
+ "total": 3,
+ "clientIds": ["client1", "client2", "client3"],
+ "clipped": false
+ }
+}
+```
+
+
+### Distinct
+
+The `distinct.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `distinct.v1` annotations.
+
+A given client can contribute to the summary for a particular annotation `name` only once, but the same client may publish additional annotations with different `name`s.
+
+Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`.
+
+
+```json
+{
+ "categories:distinct.v1": {
+ "important": {
+ "total": 2,
+ "clientIds": ["client1", "client3"],
+ "clipped": false
+ },
+ "urgent": {
+ "total": 3,
+ "clientIds": ["client1", "client2", "client3"],
+ "clipped": false
+ }
+ }
+}
+```
+
+
+### Unique
+
+The `unique.v1` summarization method counts how many unique clients have published an annotation with a given `name` for each annotation type, while guaranteeing that each client contributes to the summary for only one `name` at a time. The summary for each annotation `name` holds a `total` count of the number of distinct clients that have published an annotation with that `name` along with the corresponding list of `clientId`s that published it. Clients must be [identified](/docs/auth/identified-clients) to publish `unique.v1` annotations.
+
+A given client can contribute to the summary for a particular annotation `name` only once. Publishing an annotation with a different `name` automatically removes that client from the summary for the previous `name` and adds them to the new one, updating the affected total values and list of `clientId`s.
+
+Deleting an annotation removes the `clientId` from the list of clients that contributed to the summary for that `name`, and decrements the total count for that `name`.
+
+
+```json
+{
+ "status:unique.v1": {
+ "important": {
+ "total": 2,
+ "clientIds": ["client1", "client3"],
+ "clipped": false
+ },
+ "urgent": {
+ "total": 1,
+ "clientIds": ["client2"],
+ "clipped": false
+ }
+ }
+}
+```
+
+
+### Multiple
+
+The `multiple.v1` summarization method counts both a total and a per-client count of the number of annotations that were published with a given `name` for each annotation type. Additionally it includes a count for the number of annotations published with a given `name` from unidentified clients. Use the [identified channel rule](/docs/channels#rules) if you want to prevent unidentified clients from publishing annotations.
+
+A given client can contribute to the summary for a particular annotation `name` multiple times. The same client may also publish additional annotations with different `name`s.
+
+If a client specifies a `count` when publishing an annotation, the client's contribution to the summary is incremented by the specified value. If not, it defaults to incrementing by 1.
+
+Deleting an annotation removes all contributions made by that `clientId` for that `name`.
+
+
+```json
+{
+ "voting:multiple.v1": {
+ "option-a": {
+ "total": 7,
+ "clientCounts": {
+ "client1": 3,
+ "client2": 2
+ },
+ "totalUnidentified": 2,
+ "clipped": false,
+ "totalClientIds": 2
+ },
+ "option-b": {
+ "total": 4,
+ "clientCounts": {
+ "client1": 2,
+ "client3": 1
+ },
+ "totalUnidentified": 1,
+ "clipped": false,
+ "totalClientIds": 2
+ }
+ }
+}
+```
+
+
+## Publish annotations
+
+To publish an annotation for a message, use the `annotations.publish()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation with the action `annotation.create`.
+
+Certain annotation summarization methods require a client to be [identified](/docs/auth/identified-clients) for them to be able to publish an annotation. Their `clientId` will then be included in the associated published annotation.
+
+Specify the [annotation type](#annotation-types) using the `type` field of the annotation object.
+
+
+```javascript
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+// Publish an annotation for a message that flags it as delivered
+await channel.annotations.publish(message, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+
+// You can also use a message's `serial`
+await channel.annotations.publish(message.serial, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+```
+
+```nodejs
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+// Publish an annotation for a message that flags it as delivered
+await channel.annotations.publish(message, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+
+// You can also use a message's `serial`
+await channel.annotations.publish(message.serial, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+```
+
+
+In the case of the `distinct`, `unique`, or `multiple` aggregation types, you should also specify a `name`. For these types, each different name will be aggregated separately in the [annotation summary](#annotation-summaries).
+
+In the case of the `multiple` aggregation type, you should specify both a `name` and a `count`, by which to increment a client's contribution to the summary.
+
+
+```javascript
+await channel.annotations.publish(message.serial, {
+ type: 'rating:multiple.v1',
+ name: 'stars',
+ count: 4
+});
+```
+
+```nodejs
+await channel.annotations.publish(message.serial, {
+ type: 'rating:multiple.v1',
+ name: 'stars',
+ count: 4
+});
+```
+
+
+You can additionally specify a `data` payload when publishing an annotation. This is not included in an annotation summary, so only readable by someone [subscribing to individual annotation events](#individual-annotations).
+
+## Delete annotations
+
+To delete an annotation, use the `annotations.delete()` method on a channel. Pass in either a [message](/docs/messages) instance or the `serial` of the message to annotate. This method will publish an annotation message with an action of `annotation.delete`.
+
+Deleting an annotation does not remove the original annotation that was published. Instead, they affect the [annotation summary](#annotation-summaries) for that message by removing the contribution specified by the annotation.
+
+The `clientId` specified in the [client options](/docs/api/realtime-sdk#client-options) will be associated with the published delete annotation.
+
+Specify the [annotation type](#annotation-types) using the `type` field of the annotation object, and optionally specify a `name` for the annotation. The `name` is used to aggregate certain annotations when producing an annotation summary.
+
+
+```javascript
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+// Delete a 'delivered' annotation
+await channel.annotations.delete(message.serial, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+```
+
+```nodejs
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+// Delete a 'delivered' annotation
+await channel.annotations.delete(message.serial, {
+ type: 'receipts:flag.v1',
+ name: 'delivered'
+});
+```
+
+
+## Subscribe to annotation summaries
+
+The recommended way to receive annotation updates is through annotation summaries. These events provide a summary of the complete, current state of all annotations for a message whenever an annotation is published or deleted.
+
+Annotation summaries are delivered to subscribers as messages with an `action` of `message.summary`, and a `serial` matching the `serial` of the message that they are updating. They have an `annotations` field which contains a `summary` of all the annotations for the message.
+
+The value of that `summary` field is an object where the keys are the [annotation types](#annotation-types). The structure of the value of each key depends on the summarization method used, for example `total.v1` will have a `total` field, while `flag.v1` will have `total` and `clientIds` fields.
+
+
+
+
+```javascript
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+await channel.subscribe((message) => {
+ if (message.action === 'message.summary') {
+ console.log(message.annotations.summary);
+ }
+});
+```
+
+```nodejs
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled
+const channel = realtime.channels.get('annotations:example');
+
+await channel.subscribe((message) => {
+ if (message.action === 'message.summary') {
+ console.log(message.annotations.summary);
+ }
+});
+```
+
+
+### Annotation summaries
+
+When annotations for a message are published, Ably automatically generates a summary that provides an aggregated view of all annotations for that message.
+
+A separate summary is produced for each distinct [annotation type](#annotation-types). The summarization method specified in the annotation type determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of [individual annotation events](#individual-annotations) (annotation messages with an `action` of `annotation.create` or `annotation.delete`).
+
+The summary will be included in a `summary` field nested within the message's `annotations` field, and is an object whose keys are the annotation types and whose values describe the annotation summary for that type. For example:
+
+
+```json
+{
+ "metrics:total.v1": {
+ "total": 42
+ },
+ "reactions:flag.v1": {
+ "total": 3,
+ "clientIds": ["client1", "client2", "client3"],
+ "clipped": false
+ },
+ "categories:distinct.v1": {
+ "important": {
+ "total": 2,
+ "clientIds": ["client1", "client3"],
+ "clipped": false
+ },
+ "urgent": {
+ "total": 3,
+ "clientIds": ["client1", "client2", "client3"],
+ "clipped": false
+ }
+ },
+ "status:unique.v1": {
+ "important": {
+ "total": 2,
+ "clientIds": ["client1", "client3"],
+ "clipped": false
+ },
+ "urgent": {
+ "total": 1,
+ "clientIds": ["client2"],
+ "clipped": false
+ }
+ },
+ "voting:multiple.v1": {
+ "option-a": {
+ "total": 7,
+ "clientCounts": {
+ "client1": 3,
+ "client2": 2
+ },
+ "totalUnidentified": 2,
+ "clipped": false,
+ "totalClientIds": 2
+ },
+ "option-b": {
+ "total": 4,
+ "clientCounts": {
+ "client1": 2,
+ "client3": 1
+ },
+ "totalUnidentified": 1,
+ "clipped": false,
+ "totalClientIds": 2
+ }
+ }
+}
+```
+
+
+### Large summaries
+
+If many clients publish the same annotation to the same message, the list of client IDs in that annotation summary will get clipped in order to keep the event size within the maximum message size.
+
+When a summary is clipped:
+- The `total` property shows the total number of annotations as expected, but the `clientIds` property will contain only a partial list of client IDs.
+- The `clipped` property is set to `true`.
+- For the `multiple` annotation type, use the `totalClientIds` property to determine the total number of clients that have published the annotation. For the other annotation types this is equal to `total`.
+
+## Subscribe to individual annotation events
+
+It is also possible to subscribe to individual annotation events, rather than annotation summaries. These are the emitted when [publishing](#publish) or [deleting](#delete) an annotation.
+
+Individual events can be useful for activity feeds or detailed logging, but generally, for most usecases, subscribed clients should rely on aggregated summaries. The aggregation of annotations for a message into a summary attached to the message is the primary benefit of using the annotations API; an app design oriented around every client needing to subscribe to raw annotation events may not be taking full advantage of the feature.
+
+If you need to, you can subscribe to individual annotation events using the `annotations.subscribe()` method on a channel. To subscribe to individual annotations, you must request the `ANNOTATION_SUBSCRIBE` [mode](/docs/channels/options#modes).
+
+
+
+Annotations delivered to the `annotations.subscribe()` listener will have an `action` of `annotation.create` or `annotation.delete`.
+
+
+```javascript
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled.
+// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions.
+const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE', ... /* all other modes you need, such as MESSAGE_SUBSCRIBE */] });
+
+await channel.annotations.subscribe((annotation) => {
+ if (annotation.action === 'annotation.create') {
+ console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`);
+ } else if (annotation.action === 'annotation.delete') {
+ console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`);
+ }
+});
+```
+
+```nodejs
+// Create an Ably Realtime client specifying the clientId that will
+// be associated with annotations published by this client
+const realtime = new Ably.Realtime({ key: '{{API_KEY}}', clientId: 'my-client-id' });
+
+// Create a channel in a namespace called `annotations`
+// which has message annotations enabled.
+// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions.
+const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE'] });
+
+await channel.annotations.subscribe((annotation) => {
+ if (annotation.action === 'annotation.create') {
+ console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`);
+ } else if (annotation.action === 'annotation.delete') {
+ console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`);
+ }
+});
+```
+
+
+### Annotation message properties
+
+Annotations are a special type of message with the following properties:
+
+| Property | Description |
+| -------- | ----------- |
+| id | An Ably-generated ID used to uniquely identify the annotation. |
+| action | The action specifies whether this is an annotation being added (`annotation.create`) or removed (`annotation.delete`). |
+| serial | This annotation's unique serial (lexicographically totally ordered). |
+| messageSerial | The serial of the message that this annotation is annotating. |
+| type | The [annotation type](#annotation-types). |
+| name | The name of the annotation, used by some [annotation types](#annotation-types) for aggregation. |
+| clientId | The client identifier of the user that published this annotation. |
+| count | An optional count, only relevant to certain [annotation types](#annotation-types). |
+| data | An optional payload for the annotation. Available on an [individual annotation](#individual-annotations) but not aggregated or included in [annotation summaries](#annotation-summaries). |
+| encoding | This is typically empty, as all annotations received from Ably are automatically decoded client-side using this value. However, if the annotation encoding cannot be processed, this attribute contains the remaining transformations not applied to the `data` payload. |
+| timestamp | The timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch. |
diff --git a/src/pages/docs/messages/index.mdx b/src/pages/docs/messages/index.mdx
index e30609c3ce..5c41d59998 100644
--- a/src/pages/docs/messages/index.mdx
+++ b/src/pages/docs/messages/index.mdx
@@ -27,12 +27,15 @@ The following are the properties of a message:
|----------|-------------|
| **name** | The name of the message |
| **data** | The contents of the message. Also known as the message payload |
-| **id** | Each message sent through Ably is assigned a unique ID, unless you provide your own ID. Client specified IDs ensure [publishes are idempotent](/docs/pub-sub/advanced#idempotency) |
+| **id** | Each message sent through Ably is assigned a unique ID, unless you provide your own ID, which serves as the [idempotency key](/docs/pub-sub/advanced#idempotency) |
| **clientId** | The [ID of the client](/docs/auth/identified-clients) that published the message |
| **connectionId** | The ID of the connection used to publish the message |
-| **timestamp** | The timestamp of when the message was received by Ably, as milliseconds since the Unix epoch |
+| **timestamp** | The timestamp of when the message was first received by Ably, as milliseconds since the Unix epoch |
| **extras** | A JSON object of arbitrary key-value pairs that may contain metadata, and/or ancillary payloads. Valid payloads include those related to [Push Notifications](/docs/push), [deltas](/docs/channels/options/deltas) and headers |
| **encoding** | This is typically empty, as all messages received from Ably are automatically decoded client-side using this value. However, if the message encoding cannot be processed, this attribute contains the remaining transformations not applied to the data payload |
+| **action** | An [enum](/docs/api/realtime-sdk/types#message-action) telling you whether this is a normal ('create') message, an update to a previous message, an annotation summary, etc. |
+| **serial** | The message's serial (a server-assigned identifier that will be the same in all future updates of this message, and can be used to add [annotations](/docs/messages/annotations)). Right now this will only be set if you enable annotations in [channel rules](/docs/channels#rules) |
+| **annotations** | An object containing a summary of any [annotations](/docs/messages/annotations) that have been made to the message |
## Message conflation
diff --git a/src/pages/docs/platform/errors/codes.mdx b/src/pages/docs/platform/errors/codes.mdx
index 0e08d4d0b6..8af4bf8d46 100644
--- a/src/pages/docs/platform/errors/codes.mdx
+++ b/src/pages/docs/platform/errors/codes.mdx
@@ -765,6 +765,20 @@ You may encounter this error when the type of the object located at the specifie
**Resolution:**
* Ensure that the operation is valid for the type of object at the specified path.
+## 93001: Attempt to add an annotation listener without having requested the annotation_subscribe channel mode
+
+This error occurs when attempting to [subscribe to individual annotations](/docs/messages/annotations#subscribe-individual-annotations) without having requested the `annotation_subscribe` [channel mode](/docs/channels/options#modes) .
+
+**Resolution:**
+* Ensure that `annotation_subscribe` mode is specified in the client [channel options](/docs/channels/options) before subscribing to individual annotations.
+
+## 93002: Annotations are only supported on channels with message annotations, updates, and deletes enabled
+
+This error occurs when attempting to use [message annotations](/docs/messages/annotations) on a channel that does not have them enabled.
+
+**Resolution:**
+* Create a [channel rule](/docs/channels#rules) for the channel or channel namespace with **Message annotations, updates, and deletes** enabled.
+
## 101000: Space name missing
This error occurs when calling [`spaces.get()`](/docs/spaces/space#options) without specifying a space name. The name parameter is required to retrieve a space.
diff --git a/src/pages/docs/pub-sub/index.mdx b/src/pages/docs/pub-sub/index.mdx
index e7a9eaeae2..bbf4218150 100644
--- a/src/pages/docs/pub-sub/index.mdx
+++ b/src/pages/docs/pub-sub/index.mdx
@@ -441,4 +441,7 @@ if err := channel.Publish(context.Background(), "example", "message data"); err
You can find out more detail about how [channels](/docs/channels) and [messages](/docs/messages) work.
There are also more advanced ways that you can [subscribe](/docs/pub-sub/advanced#subscribe) to channels, and [publish](/docs/pub-sub/advanced#publish) messages, such as applying filters to your subscriptions or having a server publish messages on behalf of a client.
+
+[Annotate](/docs/messages/annotations) messages to add reactions, categorization, and other metadata to them.
+