---
source_url: https://www.pubnub.com/docs/sdks/c/api-reference/message-actions
title: Message Actions API for C SDK
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

# Message Actions API for C SDK

PubNub C SDK, use the latest version: 1.0.0

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

Message actions attach small pieces of metadata, such as receipts or emoji reactions, to a previously published message without republishing it. An action is not a new message: it always references an existing message by timetoken on a channel.

:::tip Message reactions
"Message reactions" is the same API used for a specific purpose: emoji or social reactions on a message. There is no separate reactions function. Everything on this page applies equally to receipts, reactions, or any other custom action type you define.
:::

This page covers the direct message-actions API: `pubnub_add_message_action()`, `pubnub_get_message_actions()`, and `pubnub_remove_message_action()`, plus how the same action data arrives through the subscribe listener. All three functions live in `#include <pubnub/features/message_actions.h>`, gated by `PUBNUB_ENABLE_MESSAGE_ACTIONS`. Each returns a `pubnub_future_t`, consumed with any of the SDK's three async styles: cooperative polling, blocking `pubnub_await()`, or callback-driven `pubnub_async()`. See [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md) for details.

Every sample on this page includes the specific headers it needs. `#include <pubnub/pubnub.h>` also exists as a convenience umbrella that pulls in the whole compiled-in API surface at once; it's useful for quick scripts, but the explicit per-header includes shown here make each sample's actual dependencies self-evident.

## Add message action

:::warning Requires Message Persistence
Enable Message Persistence for your key in the [Admin Portal](https://admin.pubnub.com/) as described in the [support article](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-).
:::

`pubnub_add_message_action()` adds an action to a specific message. The result carries the same action back with its server-assigned `action_timetoken`. Capture that value if you plan to remove the action later.

### Method(s)

```c
pubnub_future_t pubnub_add_message_action(pubnub_context_t* ctx,
                                           const pubnub_add_message_action_opts_t* opts);
```

`pubnub_add_message_action_opts_t` fields:

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channel | const | Yes | `—` | Borrowed, NUL-terminated. Channel the target message was published on. |
| message_timetoken | const | Yes | `—` | Borrowed, NUL-terminated. The parent message's timetoken, as a decimal string. |
| type | const | Yes | `—` | Borrowed, NUL-terminated. Free-form action type, up to 15 characters, not a closed set of values. |
| value | const | Yes | `—` | Borrowed, NUL-terminated. Action value, passed as a plain string; no manual JSON-quote wrapping is required. |
| timeout_ms | uint32_t | Optional | `0 → inherits pubnub_config_t::transaction_timeout_ms` | Non-zero overrides the context default for this call only. |

Initialize with `PUBNUB_ADD_MESSAGE_ACTION_OPTS_INIT` (equivalent to `{0}` for this struct).

:::note type is a free-form string, not an enum
`type` accepts any string up to 15 characters — `"reaction"`, `"receipt"`, or any label your application defines. There is no closed set of allowed values to choose from.
:::

**C-family contract**

* **Header** — `#include <pubnub/features/message_actions.h>`
* **Types** — `pubnub_add_message_action_opts_t`, `pubnub_add_message_action_result_t`, `pubnub_message_action_t`
* **Prerequisite** — an initialized context
* **Feature flag** — `PUBNUB_ENABLE_MESSAGE_ACTIONS`
* **Ownership / lifetime** — all four required string fields are borrowed, NUL-terminated; the result's `pubnub_message_action_t` fields alias memory owned by the future and are valid only until `pubnub_future_release`
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/error.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>
#include <pubnub/response.h>

#include <stdio.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.publish_key     = "demo";
    cfg.user_id         = "example-add-action";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        printf("pubnub_create failed\n");
        return 1;
    }

    pubnub_add_message_action_opts_t opts = PUBNUB_ADD_MESSAGE_ACTION_OPTS_INIT;
    opts.channel           = "demo_channel";
    opts.message_timetoken = "15610547826969050";
    opts.type               = "reaction";
    opts.value              = "thumbs_up";

    pubnub_future_t fut = pubnub_add_message_action(ctx, &opts);

    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_add_message_action_result_t r = pubnub_add_message_action_result(fut);
        printf("Action added successfully.\n");
        printf("  type:              %.*s\n", (int)r.action.type.len, r.action.type.ptr);
        printf("  value:             %.*s\n", (int)r.action.value.len, r.action.value.ptr);
        printf("  uuid:              %.*s\n", (int)r.action.uuid.len, r.action.uuid.ptr);
        printf("  action_timetoken:  %.*s\n", (int)r.action.action_timetoken.len, r.action.action_timetoken.ptr);
        printf("  message_timetoken: %.*s\n", (int)r.action.message_timetoken.len, r.action.message_timetoken.ptr);
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("add_message_action failed: %s (%.*s)\n", pubnub_res_str(status), (int)err.len, err.ptr);
    }

    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

Adapted from `examples/message_actions/add_message_action.c` (cooperative polling).

### Returns

`pubnub_add_message_action_result()` returns a `pubnub_add_message_action_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `action` | `pubnub_message_action_t` | The added action, with server-assigned timetokens. |

`pubnub_message_action_t` fields (also used by [Get message actions](#get-message-actions)):

| Field | Type | Description |
| --- | --- | --- |
| `type` | `pubnub_string_view_t` | Action type, e.g. `"reaction"`. |
| `value` | `pubnub_string_view_t` | Action value, e.g. `"thumbs_up"`. |
| `uuid` | `pubnub_string_view_t` | User ID of the publisher who added the action. |
| `action_timetoken` | `pubnub_string_view_t` | Server-assigned timetoken identifying this action. There is no separate action ID. Use this value in [Remove message action](#remove-message-action). |
| `message_timetoken` | `pubnub_string_view_t` | Timetoken of the parent message the action was added to. |

Every `pubnub_string_view_t` field aliases internal buffers and remains valid only until `pubnub_future_release()` is called on the owning future. Copy out any bytes you need after that point.

### Other examples

#### Add message action, blocking

```c
#include <pubnub/client.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.publish_key   = "demo";
    cfg.subscribe_key = "demo";
    cfg.user_id       = "example-add-action-blocking";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        return EXIT_FAILURE;
    }

    pubnub_add_message_action_opts_t opts = PUBNUB_ADD_MESSAGE_ACTION_OPTS_INIT;
    opts.channel           = "demo_channel";
    opts.message_timetoken = "15610547826969050";
    opts.type               = "reaction";
    opts.value              = "thumbs_up";

    pubnub_future_t future = pubnub_add_message_action(ctx, &opts);
    pubnub_res_t     result = pubnub_await(future);

    if (PUBNUB_OK == result) {
        pubnub_add_message_action_result_t r = pubnub_add_message_action_result(future);
        printf("action_timetoken: %.*s\n", (int)r.action.action_timetoken.len, r.action.action_timetoken.ptr);
    }
    else {
        printf("add_message_action failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
    pubnub_destroy(ctx);
    return EXIT_SUCCESS;
}
```

### Error responses

| Condition | Result |
| --- | --- |
| `opts` is `NULL` | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `channel` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `message_timetoken` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `type` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `type` exceeds 15 characters | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `value` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |

These checks run synchronously before any request is sent. On validation failure, `pubnub_add_message_action()` returns `PUBNUB_FUTURE_INVALID` instead of a future tied to a real request. `PUBNUB_FUTURE_INVALID` is still a valid value to pass to `pubnub_future_status()`, which reports `PUBNUB_ERR_INVALID_ARGUMENT` for it. So the same check you use for any other failure catches this case too, and you do not need to compare the future against the sentinel directly. For the full `pubnub_res_t` catalog and how to read server-side error detail, see [The pubnub_res_t result catalog](https://www.pubnub.com/docs/sdks/c/status-events.md#the-pubnub_res_t-result-catalog) and [Retrieving server error detail](https://www.pubnub.com/docs/sdks/c/status-events.md#retrieving-server-error-detail).

## Remove message action

:::warning Requires Message Persistence
Enable Message Persistence for your key in the [Admin Portal](https://admin.pubnub.com/) as described in the [support article](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-).
:::

:::note No action ID
An action has no ID of its own. Removal is identified by the triple `{channel, message_timetoken, action_timetoken}`: the channel name, the parent message's timetoken, and the action's own server-assigned `action_timetoken` from a prior [Add message action](#add-message-action) result.
:::

`pubnub_remove_message_action()` removes a previously added action. On success, the response body is empty. There is no result accessor for this function.

:::note Server-enforced removal rule
Only the user who originally added an action may remove it. The server enforces this rule. The client SDK does not check it locally before sending the request.
:::

### Method(s)

```c
pubnub_future_t
pubnub_remove_message_action(pubnub_context_t*                          ctx,
                              const pubnub_remove_message_action_opts_t* opts);
```

`pubnub_remove_message_action_opts_t` fields:

| Parameter | Description |
| --- | --- |
| `channel` *Type: `const char*`Default: — | Borrowed, NUL-terminated. |
| `message_timetoken` *Type: `const char*`Default: — | Borrowed, NUL-terminated. The parent message's timetoken. |
| `action_timetoken` *Type: `const char*`Default: — | Borrowed, NUL-terminated. The action's own timetoken to remove — typically the `action_timetoken` captured from a prior add result. |
| `timeout_ms`Type: `uint32_t`Default: `0` → inherits `pubnub_config_t::transaction_timeout_ms` | Non-zero overrides the context default for this call only. |

Initialize with `PUBNUB_REMOVE_MESSAGE_ACTION_OPTS_INIT` (equivalent to `{0}` for this struct).

**C-family contract**

* **Header** — `#include <pubnub/features/message_actions.h>`
* **Types** — `pubnub_remove_message_action_opts_t`
* **Prerequisite** — an initialized context
* **Feature flag** — `PUBNUB_ENABLE_MESSAGE_ACTIONS`
* **Ownership / lifetime** — all three required string fields are borrowed, NUL-terminated
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/error.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>
#include <pubnub/response.h>

#include <stdio.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.publish_key     = "demo";
    cfg.user_id         = "example-remove-action";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        printf("pubnub_create failed\n");
        return 1;
    }

    pubnub_remove_message_action_opts_t opts = PUBNUB_REMOVE_MESSAGE_ACTION_OPTS_INIT;
    opts.channel           = "demo_channel";
    opts.message_timetoken = "15610547826969050";
    opts.action_timetoken  = "15610547826970050";

    pubnub_future_t fut = pubnub_remove_message_action(ctx, &opts);

    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        printf("Action removed successfully.\n");
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("remove_message_action failed: %s (%.*s)\n", pubnub_res_str(status), (int)err.len, err.ptr);
    }

    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

Adapted from `examples/message_actions/remove_message_action.c` (cooperative polling). That example uses a hardcoded `action_timetoken` literal. The sample below chains a real one from an add call instead.

### Returns

`pubnub_remove_message_action()` has no result accessor of its own. Success is signaled by the future's status alone: check `pubnub_future_status(fut) == PUBNUB_OK`. There is no data to extract on success.

### Other examples

#### Chain an add result into a remove

`remove_message_action.c` uses a hardcoded `action_timetoken` literal. The sample below instead chains a real one captured from an add call.

```c
#include <pubnub/client.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.publish_key   = "demo";
    cfg.subscribe_key = "demo";
    cfg.user_id       = "example-chain-add-remove";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        return EXIT_FAILURE;
    }

    /* 1. Add the action and capture its server-assigned action_timetoken. */
    pubnub_add_message_action_opts_t add_opts = PUBNUB_ADD_MESSAGE_ACTION_OPTS_INIT;
    add_opts.channel           = "demo_channel";
    add_opts.message_timetoken = "15610547826969050";
    add_opts.type               = "reaction";
    add_opts.value              = "thumbs_up";

    pubnub_future_t add_future = pubnub_add_message_action(ctx, &add_opts);
    while (!pubnub_future_is_ready(add_future)) {
        pubnub_process(ctx);
    }

    if (PUBNUB_OK != pubnub_future_status(add_future)) {
        pubnub_future_release(add_future);
        pubnub_destroy(ctx);
        return EXIT_FAILURE;
    }

    pubnub_add_message_action_result_t added = pubnub_add_message_action_result(add_future);

    /* Copy the action_timetoken out before releasing add_future - the view
       is only valid until pubnub_future_release() is called on that future. */
    char   action_tt_buf[24] = {0};
    size_t tt_len = added.action.action_timetoken.len < 23 ? added.action.action_timetoken.len : 23;
    memcpy(action_tt_buf, added.action.action_timetoken.ptr, tt_len);
    action_tt_buf[tt_len] = '\0';

    pubnub_future_release(add_future);

    /* 2. Remove the action just added, using the captured timetoken. */
    pubnub_remove_message_action_opts_t remove_opts = PUBNUB_REMOVE_MESSAGE_ACTION_OPTS_INIT;
    remove_opts.channel           = "demo_channel";
    remove_opts.message_timetoken = "15610547826969050";
    remove_opts.action_timetoken  = action_tt_buf;

    pubnub_future_t remove_future = pubnub_remove_message_action(ctx, &remove_opts);
    while (!pubnub_future_is_ready(remove_future)) {
        pubnub_process(ctx);
    }

    if (PUBNUB_OK == pubnub_future_status(remove_future)) {
        printf("Action removed successfully.\n");
    }

    pubnub_future_release(remove_future);
    pubnub_destroy(ctx);
    return 0;
}
```

### Error responses

| Condition | Result |
| --- | --- |
| `opts` is `NULL` | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `channel`, `message_timetoken`, or `action_timetoken` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |

On any of these, `pubnub_remove_message_action()` returns `PUBNUB_FUTURE_INVALID` rather than a future tied to a real request. Checking `pubnub_future_status()` still reports `PUBNUB_ERR_INVALID_ARGUMENT` for it, so the usual status check catches this case without a separate comparison against the sentinel. See [The pubnub_res_t result catalog](https://www.pubnub.com/docs/sdks/c/status-events.md#the-pubnub_res_t-result-catalog) for the full set of possible result values.

## Get message actions

:::warning Requires Message Persistence
Enable Message Persistence for your key in the [Admin Portal](https://admin.pubnub.com/) as described in the [support article](https://support.pubnub.com/hc/en-us/articles/360051974791-How-do-I-enable-add-on-features-for-my-keys-).
:::

`pubnub_get_message_actions()` retrieves actions added to messages on a channel.

### Method(s)

```c
pubnub_future_t pubnub_get_message_actions(pubnub_context_t* ctx,
                                            const pubnub_get_message_actions_opts_t* opts);
```

`pubnub_get_message_actions_opts_t` fields:

| Parameter | Description |
| --- | --- |
| `channel` *Type: `const char*`Default: — | Borrowed, NUL-terminated. |
| `start`Type: `const char*`Default: `NULL` (omit) | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Exclusive upper bound**: only actions with timetokens less than `start` are returned. Same rule as `start` on `pubnub_fetch_messages()`. |
| `end`Type: `const char*`Default: `NULL` (omit) | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Inclusive lower bound**: actions with timetokens greater than or equal to `end` are returned. Same rule as `end` on `pubnub_fetch_messages()`. |
| `limit`Type: `uint32_t`Default: `0` → server default | Maximum results to return. The header states no upper ceiling. |
| `timeout_ms`Type: `uint32_t`Default: `0` → inherits `pubnub_config_t::transaction_timeout_ms` | Non-zero overrides the context default for this call only. |

Initialize with `PUBNUB_GET_MESSAGE_ACTIONS_OPTS_INIT` (equivalent to `{0}` for this struct).

**C-family contract**

* **Header** — `#include <pubnub/features/message_actions.h>`
* **Types** — `pubnub_get_message_actions_opts_t`, `pubnub_get_message_actions_result_t`, `pubnub_message_action_t`
* **Prerequisite** — an initialized context
* **Feature flag** — `PUBNUB_ENABLE_MESSAGE_ACTIONS`
* **Ownership / lifetime** — `channel`/`start`/`end` are borrowed, NUL-terminated strings; every `pubnub_string_view_t` field on the result and every action returned by the indexed accessor aliases memory owned by the future and is valid only until `pubnub_future_release`
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

:::warning Copy pagination cursors before releasing the future
The result's `more_start` and `more_end` fields are views into the completed future's internal storage. If you release the future before copying them out, the next call reads freed memory. Copy `more_start`/`more_end` into your own buffers first, release the future, then reassign `opts.start`/`opts.end` to point at your buffers before issuing the next call.
:::

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/error.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>
#include <pubnub/response.h>

#include <stdint.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-get-actions";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        printf("pubnub_create failed\n");
        return 1;
    }

    pubnub_get_message_actions_opts_t opts = PUBNUB_GET_MESSAGE_ACTIONS_OPTS_INIT;
    opts.channel = "demo_channel";
    opts.limit   = 10;

    int  page          = 0;
    int  more          = 1;
    char start_buf[24] = {0};
    char end_buf[24]   = {0};

    while (more) {
        pubnub_future_t fut = pubnub_get_message_actions(ctx, &opts);

        while (!pubnub_future_is_ready(fut)) {
            pubnub_process(ctx);
        }

        const pubnub_res_t status = pubnub_future_status(fut);
        if (PUBNUB_OK != status) {
            pubnub_string_view_t err = pubnub_response_error_message(fut);
            printf("get_message_actions failed: %s (%.*s)\n", pubnub_res_str(status), (int)err.len, err.ptr);
            pubnub_future_release(fut);
            break;
        }

        pubnub_get_message_actions_result_t r = pubnub_get_message_actions_result(fut);

        printf("--- Page %d: %u actions ---\n", ++page, r.count);

        for (uint32_t i = 0; i < r.count; ++i) {
            pubnub_message_action_t a = pubnub_get_message_actions_result_action_at(fut, i);
            printf("  [%u] type=%.*s value=%.*s uuid=%.*s att=%.*s\n",
                   i,
                   (int)a.type.len, a.type.ptr,
                   (int)a.value.len, a.value.ptr,
                   (int)a.uuid.len, a.uuid.ptr,
                   (int)a.action_timetoken.len, a.action_timetoken.ptr);
        }

        if (r.has_more && NULL != r.more_start.ptr) {
            /* Copy cursor strings before releasing the future, since the
               view data is only valid until pubnub_future_release(). */
            size_t slen = r.more_start.len < 23 ? r.more_start.len : 23;
            memcpy(start_buf, r.more_start.ptr, slen);
            start_buf[slen] = '\0';
            opts.start      = start_buf;

            if (NULL != r.more_end.ptr) {
                size_t elen = r.more_end.len < 23 ? r.more_end.len : 23;
                memcpy(end_buf, r.more_end.ptr, elen);
                end_buf[elen] = '\0';
                opts.end      = end_buf;
            }
        }
        else {
            more = 0;
        }

        pubnub_future_release(fut);
    }

    pubnub_destroy(ctx);
    return 0;
}
```

Adapted from `examples/message_actions/get_message_actions.c` (cooperative polling with a pagination loop). Note the order: cursor bytes are copied into `start_buf`/`end_buf` **before** `pubnub_future_release(fut)` runs.

### Returns

`pubnub_get_message_actions_result()` returns a `pubnub_get_message_actions_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `count` | `uint32_t` | Number of actions in this page; the upper bound for `pubnub_get_message_actions_result_action_at()`. |
| `has_more` | `uint8_t` | Non-zero when the server indicates more results exist beyond this page. This is the only signal that another page exists. There is no total count of remaining actions. |
| `more_start` | `pubnub_string_view_t` | Server-recommended cursor. Copy into your own buffer and pass as `opts.start` for the next call. |
| `more_end` | `pubnub_string_view_t` | Server-recommended cursor. Copy into your own buffer and pass as `opts.end` for the next call. |
| `more_limit` | `uint32_t` | Server-recommended `limit` for the next call. |

Read individual actions with `pubnub_get_message_actions_result_action_at(future, index)`, where `index` is in `[0, count)`. An out-of-range or invalid index returns a zero-initialized `pubnub_message_action_t`. See the [pubnub_message_action_t field table](#returns) under Add message action for its fields.

### Other examples

#### Get message actions, single page

A single call without a pagination loop, consumed with blocking `pubnub_await()`:

```c
#include <pubnub/client.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id       = "example-get-actions-single-page";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        return EXIT_FAILURE;
    }

    pubnub_get_message_actions_opts_t opts = PUBNUB_GET_MESSAGE_ACTIONS_OPTS_INIT;
    opts.channel = "demo_channel";
    opts.limit   = 25;

    pubnub_future_t future = pubnub_get_message_actions(ctx, &opts);
    pubnub_res_t     result = pubnub_await(future);

    if (PUBNUB_OK == result) {
        pubnub_get_message_actions_result_t r = pubnub_get_message_actions_result(future);
        for (uint32_t i = 0; i < r.count; ++i) {
            pubnub_message_action_t a = pubnub_get_message_actions_result_action_at(future, i);
            printf("type=%.*s value=%.*s\n", (int)a.type.len, a.type.ptr, (int)a.value.len, a.value.ptr);
        }
        if (r.has_more) {
            printf("More actions are available; call again with opts.start/opts.end set to more_start/more_end.\n");
        }
    }

    pubnub_future_release(future);
    pubnub_destroy(ctx);
    return EXIT_SUCCESS;
}
```

#### Get message actions, async callback

```c
#include <pubnub/client.h>
#include <pubnub/error.h>
#include <pubnub/features/message_actions.h>
#include <pubnub/future.h>
#include <pubnub/response.h>

#include <stdint.h>
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
#define PN_DOC_SLEEP_MS(ms) Sleep((DWORD)(ms))
#else
#include <unistd.h>
#define PN_DOC_SLEEP_MS(ms) usleep((unsigned)(ms) * 1000U)
#endif

static volatile int s_done;

static void on_get_complete(pubnub_future_t future, pubnub_res_t status, void* user_data)
{
    (void)user_data;

    if (PUBNUB_OK == status) {
        pubnub_get_message_actions_result_t r = pubnub_get_message_actions_result(future);
        printf("  [callback] Got %u actions\n", r.count);

        for (uint32_t i = 0; i < r.count; ++i) {
            pubnub_message_action_t a = pubnub_get_message_actions_result_action_at(future, i);
            printf("    [%u] %.*s: %.*s (by %.*s)\n",
                   i,
                   (int)a.type.len, a.type.ptr,
                   (int)a.value.len, a.value.ptr,
                   (int)a.uuid.len, a.uuid.ptr);
        }

        if (r.has_more) {
            printf("  [callback] More pages available (start=%.*s)\n", (int)r.more_start.len, r.more_start.ptr);
        }
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("  [callback] get_message_actions failed: %s (%.*s)\n", pubnub_res_str(status), (int)err.len, err.ptr);
    }

    pubnub_future_release(future);
    s_done = 1;
}

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-get-actions-async";

    pubnub_context_t* ctx = pubnub_create(&cfg);
    if (NULL == ctx) {
        printf("pubnub_create failed\n");
        return 1;
    }

    pubnub_get_message_actions_opts_t opts = PUBNUB_GET_MESSAGE_ACTIONS_OPTS_INIT;
    opts.channel = "demo_channel";
    opts.limit   = 5;

    pubnub_future_t fut = pubnub_get_message_actions(ctx, &opts);

    pubnub_res_t rc = pubnub_async(fut, on_get_complete, NULL);
    if (PUBNUB_OK != rc) {
        printf("pubnub_async failed: %s\n", pubnub_res_str(rc));
        pubnub_future_release(fut);
        pubnub_destroy(ctx);
        return 1;
    }

    while (!s_done) {
        /* On a threaded platform (PUBNUB_CFG_THREAD_SAFETY=1), a background
           thread drives I/O here and this sleep is just a wait. On embedded
           builds without threads, replace this sleep with a call to
           pubnub_process(ctx) in the loop instead -- otherwise the
           callback never fires. */
        PN_DOC_SLEEP_MS(50);
    }

    pubnub_destroy(ctx);
    return 0;
}
```

Adapted from `examples/message_actions/get_message_actions_async.c`. On platforms with `PUBNUB_CFG_THREAD_SAFETY=1`, `pubnub_async()` starts a background thread that drives I/O automatically. On embedded targets without threads, replace the sleep loop with a call to `pubnub_process(ctx)` in a loop.

### Error responses

| Condition | Result |
| --- | --- |
| `opts` is `NULL` | `PUBNUB_ERR_INVALID_ARGUMENT` |
| `channel` is `NULL` or empty | `PUBNUB_ERR_INVALID_ARGUMENT` |

On either of these, `pubnub_get_message_actions()` returns `PUBNUB_FUTURE_INVALID` rather than a future tied to a real request. `pubnub_future_status()` still reports `PUBNUB_ERR_INVALID_ARGUMENT` for it. No maximum value is enforced or documented for `limit` in this SDK version. For the full `pubnub_res_t` catalog and server-error retrieval pattern, see [The pubnub_res_t result catalog](https://www.pubnub.com/docs/sdks/c/status-events.md#the-pubnub_res_t-result-catalog) and [Retrieving server error detail](https://www.pubnub.com/docs/sdks/c/status-events.md#retrieving-server-error-detail).

## Receiving message actions in real time

Beyond calling `pubnub_get_message_actions()`, a subscribed context also receives message-action events live, the same way it receives messages, signals, and presence events. When another client adds or removes an action on a channel this context is subscribed to, the subscribe event engine delivers an event of type `PUBNUB_EVENT_TYPE_MESSAGE_ACTION`. `pubnub_subscribe_event_message_action()` extracts that event into a typed struct.

This is a third, independently-shaped representation of "a message action", distinct from `pubnub_message_action_t` (the direct API above) and from the raw JSON node returned inside fetched history (see [Message actions inside fetched history](#message-actions-inside-fetched-history)). There is no conversion function between any of the three.

### Method(s)

```c
pubnub_res_t pubnub_subscribe_event_message_action(
    pubnub_context_t*                        ctx,
    const pubnub_subscribe_event_t*          event,
    pubnub_subscribe_message_action_event_t* out);
```

```c
typedef enum pubnub_message_action_type {
    PUBNUB_MESSAGE_ACTION_ADDED   = 0, /* A reaction or action was added to a message. */
    PUBNUB_MESSAGE_ACTION_REMOVED = 1  /* A reaction or action was removed from a message. */
} pubnub_message_action_type_t;
```

`pubnub_subscribe_message_action_event_t` fields:

| Field | Type | Description |
| --- | --- | --- |
| `event` | `pubnub_message_action_type_t` | Whether the action was added or removed. |
| `channel` | `pubnub_string_view_t` | Channel on which the action occurred. |
| `subscription` | `pubnub_string_view_t` | Subscription match pattern. |
| `publisher` | `pubnub_string_view_t` | Publisher of the original message. |
| `message_timetoken` | `pubnub_string_view_t` | Timetoken of the message being acted on. |
| `action_timetoken` | `pubnub_string_view_t` | Timetoken when the action itself was created. |
| `type` | `pubnub_string_view_t` | Action type, e.g. `"reaction"`. |
| `value` | `pubnub_string_view_t` | Action value, e.g. an emoji string. |

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>` and `#include <pubnub/features/subscribe_types.h>`
* **Types** — `pubnub_message_action_type_t`, `pubnub_subscribe_message_action_event_t`
* **Prerequisite** — a live subscription delivering events to a listener callback
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`, independent of `PUBNUB_ENABLE_MESSAGE_ACTIONS` — this extractor works even in a build where the direct message-actions API is compiled out
* **Ownership / lifetime** — `out` is caller-owned stack memory the extractor copies into, but its `pubnub_string_view_t` fields alias the raw event's backing storage, valid only for the duration of the listener callback. This is narrower than the future-release rule that governs the direct-API structs above. Do not retain these views past the callback.
* **Blocking** — synchronous; called directly inside the listener callback, no future involved

Returns `PUBNUB_OK` on success, `PUBNUB_ERR_INVALID_ARGUMENT` if any argument is `NULL` or the event is not a message-action event, and `PUBNUB_ERR_SERIALIZATION` on parse failure.

### Sample code

```c
static void on_message_action(const pubnub_subscribe_event_t* event, void* user_data)
{
    pubnub_context_t* ctx = (pubnub_context_t*)user_data;

    pubnub_subscribe_message_action_event_t ma;
    if (PUBNUB_OK == pubnub_subscribe_event_message_action(ctx, event, &ma)) {
        printf("action type: %.*s, value: %.*s\n",
               (int)ma.type.len, ma.type.ptr,
               (int)ma.value.len, ma.value.ptr);
    }
}
```

This extractor is one of six typed subscribe-event extractors. For the full worked example, registering a listener, creating a subscription, and wiring this callback into it, see [Message action](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#message-action) in Publish & Subscribe, which owns the general listener-registration mechanics.

### Returns

There is no separate result type beyond the `out` struct populated above. Read its fields directly, subject to the callback-duration lifetime described in the C-family contract.

## Message actions inside fetched history

`pubnub_fetch_messages()` can embed each message's actions in its own result when the request sets `include_message_actions = 1`. That path returns a raw, untyped `pubnub_json_value_t*` node from `pubnub_fetch_messages_result_actions_at()`, not a `pubnub_message_action_t`. The two features compile fully independently of each other (`PUBNUB_ENABLE_MESSAGE_ACTIONS` has no effect on `PUBNUB_ENABLE_HISTORY` or vice versa). See [Fetch messages with message actions](https://www.pubnub.com/docs/sdks/c/api-reference/storage-and-playback.md#fetch-messages-with-message-actions) in Storage & Playback for that field, the accessor, and how to walk the resulting JSON tree. This page does not duplicate that content.

## Terms in this document

* **Channel** - A pathway for sending and receiving messages between devices, created automatically when you first use it, that can handle any number of users and messages for different communication needs, like 1-1 text chats, group conversations, and other data streaming.
* **Channel (DataSync)** - A built-in entity class in DataSync that stores channel metadata as a JSON payload. The same channel used for messaging and Presence.
* **Channel pattern** - A way to group and analyze channel data to track performance metrics like message counts and user engagement over time with PubNub Insights.
* **Message** - A unit of data transmitted between clients or between a client and a server in PubNub, containing information such as text, binary data, or structured data formats like JSON. Messages are sent over channels and can be tracked for delivery and read status.
* **Timetoken** - A unique identifier for each message that represents the number of 100-nanosecond intervals since January 1, 1970, for example, 16200000000000000.

Last updated at: 2026-09-24T12:25:41.000Z
