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

# Message Persistence 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 Persistence gives you real-time access to the history of messages published to PubNub. Each message is timestamped to the nearest 10 nanoseconds and stored across multiple availability zones in several geographic locations. You can encrypt stored messages with AES-256 so they are not readable on PubNub’s network. For details, see [Message Persistence](https://www.pubnub.com/docs/general/storage.md).

You control how long messages are stored through your account’s retention policy. Options include: 1 day, 7 days, 30 days, 3 months, 6 months, 1 year, or Unlimited.

You can retrieve the following:

* Messages
* Message reactions
* Files (using the File Sharing API)

This SDK has exactly one history-retrieval entry point, `pubnub_fetch_messages()`. There is no separate deprecated function alongside it, so this page has no "(old)" section to contrast against.

## Fetch messages

`pubnub_fetch_messages()` retrieves stored messages from one or more channels, optionally with metadata, message actions, and file-message details.

### Method(s)

```c
pubnub_future_t pubnub_fetch_messages(pubnub_context_t* ctx, const pubnub_fetch_messages_opts_t* opts);
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channels | const | Yes | `—` | Borrowed, NUL-terminated. Comma-separated (e.g. `"ch1,ch2,ch3"`). When `include_message_actions` is set, only a single channel is allowed. |
| start | const | Optional | `NULL (fetch from the newest available message)` | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Exclusive.** Only messages older than `start` are returned. |
| end | const | Optional | `NULL` | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Inclusive.** Messages newer than or equal to `end` are returned. |
| count | uint16_t | Optional | `0 (server default: 25 for multi-channel or with-actions requests, 100 for a single channel)` | Clamped server-side to those same maximums regardless of what you request. |
| reverse | uint8_t | Optional | `0 (newest first)` | Non-zero returns the array oldest first. This controls the actual sort order of the returned messages, not only which end of the range paging starts from. |
| include_meta | uint8_t | Optional | `0 (omit)` | Non-zero includes the `meta` field in each result. |
| include_uuid | uint8_t | Optional | `1 via PUBNUB_FETCH_MESSAGES_OPTS_INIT` | Includes the publisher's `user_id` (reported as `uuid` on the result). |
| include_message_type | uint8_t | Optional | `1 via PUBNUB_FETCH_MESSAGES_OPTS_INIT` | Includes the event-type discriminator on each result. |
| include_custom_message_type | uint8_t | Optional | `0 (omit)` | Includes the publisher-supplied custom message type label. |
| include_message_actions | uint8_t | Optional | `0 (omit)` | Routes the request to the history-with-actions endpoint. Only a single channel is allowed, and `count` clamps to 25. |
| timeout_ms | uint32_t | Optional | `0 (inherits pubnub_config_t::transaction_timeout_ms)` | A non-zero value overrides the context-level timeout for this call only. |

Zero-initializing this struct by hand (`= {0}`) silently turns `include_uuid` and `include_message_type` off. Always initialize with `PUBNUB_FETCH_MESSAGES_OPTS_INIT`, which sets those two fields to `1` and zeroes the rest. It is the only history options macro that is not a flat `{0}`.

**C-family contract**

* **Header** — `#include <pubnub/features/history.h>`
* **Types** — `pubnub_fetch_messages_opts_t`, `pubnub_fetch_messages_result_t`, `pubnub_fetch_messages_channel_result_t`, `pubnub_history_message_result_t`
* **Prerequisite** — an initialized context with `subscribe_key` set
* **Feature flag** — `PUBNUB_ENABLE_HISTORY`
* **Ownership / lifetime** — `channels`/`start`/`end` are borrowed, NUL-terminated strings; every pointer/view field on the result aliases memory owned by the future and is valid only until `pubnub_future_release`, with a narrower rule for decrypted content (see [Returns](#returns) below)
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

```c
#include <pubnub/client.h>
#include <pubnub/features/history.h>
#include <pubnub/future.h>
#include <pubnub/providers/serialization.h>
#include <pubnub/response.h>

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

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id        = "my_unique_user_id";

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

    pubnub_fetch_messages_opts_t opts = PUBNUB_FETCH_MESSAGES_OPTS_INIT;
    opts.channels     = "ch1,ch2";
    opts.count        = 10;
    opts.include_meta = 1;

    pubnub_future_t future = pubnub_fetch_messages(ctx, &opts);
    while (!pubnub_future_is_ready(future)) {
        pubnub_process(ctx);
    }

    if (PUBNUB_OK == pubnub_future_status(future)) {
        pubnub_fetch_messages_result_t r = pubnub_fetch_messages_result(future);
        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_fetch_messages_channel_result_t ch =
                pubnub_fetch_messages_result_channel_at(future, i);
            printf("[%.*s] %u messages\n", (int)ch.name.len, ch.name.ptr, ch.message_count);

            for (size_t j = 0; j < ch.message_count; ++j) {
                pubnub_history_message_result_t msg =
                    pubnub_fetch_messages_result_message_at(future, i, j);

                if (PUBNUB_EVENT_TYPE_FILE == msg.event_type) {
                    pubnub_history_file_result_t file =
                        pubnub_fetch_messages_result_file_at(future, i, j);
                    printf("  FILE %.*s (id=%.*s)\n",
                           (int)file.name.len, file.name.ptr,
                           (int)file.id.len, file.id.ptr);
                }
                else {
                    pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
                    if (NULL != msg.message && NULL != serial
                        && NULL != serial->value_as_string) {
                        size_t      mlen = 0;
                        const char* mptr = serial->value_as_string(msg.message, &mlen);
                        if (NULL != mptr) {
                            printf("  %.*s: %.*s\n",
                                   (int)msg.timetoken.len, msg.timetoken.ptr,
                                   (int)mlen, mptr);
                        }
                    }
                }
            }
        }
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("fetch failed: %.*s\n", (int)err.len, err.ptr);
    }

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

Adapted from `examples/history/fetch_messages.c` (cooperative polling), with the NULL-checks on `serial` and `msg.message` added. The header's own `@code` example for this function reads `msg.message` through `serial->value_as_string()` without either check. The pattern of getting the serialization provider, checking each vtable accessor, and reading through `value_as_string()` is the same one used for subscribe events. See [Receiving messages — reading a JSON payload](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#receiving-messages--reading-a-json-payload) for the full explanation, including how to read object and array fields.

### Returns

```c
pubnub_fetch_messages_result_t          pubnub_fetch_messages_result(pubnub_future_t future);
pubnub_fetch_messages_channel_result_t  pubnub_fetch_messages_result_channel_at(pubnub_future_t future, size_t index);
pubnub_history_message_result_t         pubnub_fetch_messages_result_message_at(pubnub_future_t future, size_t channel_index, size_t message_index);
pubnub_history_file_result_t            pubnub_fetch_messages_result_file_at(pubnub_future_t future, size_t channel_index, size_t message_index);
const pubnub_json_value_t*              pubnub_fetch_messages_result_actions_at(pubnub_future_t future, size_t channel_index, size_t message_index);
```

Reading a result is two-level indexed access, not a single flat list. Call `pubnub_fetch_messages_result()` first. This triggers lazy parsing of the response. Then loop channels with `pubnub_fetch_messages_result_channel_at()` up to `channel_count`, and loop messages within each channel with `pubnub_fetch_messages_result_message_at()` up to that channel's `message_count`. Every accessor returns a zero-initialized struct for an out-of-range index or a future that is not ready.

`pubnub_fetch_messages_channel_result_t` has `name` (`pubnub_string_view_t`) and `message_count` (`uint32_t`). **Channel iteration order does not necessarily match the order channels were requested in.** It depends on the server response's own JSON object order. If you need a specific channel's results, search for it by `name` rather than assuming an index.

`pubnub_history_message_result_t` has 7 fields:

| Field | Type | Description |
| --- | --- | --- |
| `message` | `const pubnub_json_value_t*` | JSON-node pointer, not a string. `NULL` when absent. |
| `timetoken` | `pubnub_timetoken_t` | The server-assigned publish timetoken, as a non-NUL-terminated view over 17-digit decimal text. Print with `%.*s`. |
| `uuid` | `pubnub_string_view_t` | Publisher's `user_id`. Empty (`.len == 0`) when `include_uuid` was not set. |
| `meta` | `const pubnub_json_value_t*` | JSON-node pointer. `NULL` when `include_meta` was not set or the message carried no metadata. |
| `custom_message_type` | `pubnub_string_view_t` | Empty when `include_custom_message_type` was not set or the publisher did not supply one. |
| `event_type` | `pubnub_event_type_t` | See [The pubnub_event_type_t enum](#the-pubnub_event_type_t-enum) below. `pubnub_fetch_messages()` only ever produces `PUBNUB_EVENT_TYPE_MESSAGE`, `PUBNUB_EVENT_TYPE_FILE`, or `PUBNUB_EVENT_TYPE_UNKNOWN` (the last when `include_message_type` was not set in the request). |
| `crypto_result` | `pubnub_res_t` | `PUBNUB_OK` when the message needed no decryption or decrypted successfully. `PUBNUB_ERR_CRYPTO` when a crypto module is configured but this specific message failed to decrypt. |

Every pointer/view field above aliases memory owned by the future and is valid only until `pubnub_future_release`.

#### The pubnub_event_type_t enum

The header documents `pubnub_event_type_t` as a discriminator shared conceptually between history and subscribe, mirroring the server's wire-level event type field. Its full set of values, including the two that never appear on the wire:

| Value | Meaning | Produced by |
| --- | --- | --- |
| `PUBNUB_EVENT_TYPE_UNKNOWN` (`-2`) | The field was not populated. | History only, when `include_message_type` is not set on the request. |
| `PUBNUB_EVENT_TYPE_PRESENCE` (`-1`) | Presence event. SDK-internal, never a wire value. | Not produced by `pubnub_fetch_messages()`. |
| `PUBNUB_EVENT_TYPE_MESSAGE` (`0`) | Regular published message. | `pubnub_fetch_messages()`. |
| `PUBNUB_EVENT_TYPE_SIGNAL` (`1`) | Signal. | Not produced by `pubnub_fetch_messages()`. |
| `PUBNUB_EVENT_TYPE_OBJECTS` (`2`) | App Context (Objects) event. | Not produced by `pubnub_fetch_messages()`. |
| `PUBNUB_EVENT_TYPE_MESSAGE_ACTION` (`3`) | Message action added or removed. | Not produced by `pubnub_fetch_messages()`. |
| `PUBNUB_EVENT_TYPE_FILE` (`4`) | File-sharing event. | `pubnub_fetch_messages()`. |

In practice, `pubnub_fetch_messages()` only ever sets `event_type` to `PUBNUB_EVENT_TYPE_MESSAGE`, `PUBNUB_EVENT_TYPE_FILE`, or `PUBNUB_EVENT_TYPE_UNKNOWN`. The other values exist because the enum also describes categories the subscribe stream can carry. Subscribe reports its own event type through a different, similarly-named type (see below). Call `pubnub_fetch_messages_result_file_at()` when `event_type == PUBNUB_EVENT_TYPE_FILE` to read the file's metadata instead of a generic message body.

Do not confuse `pubnub_event_type_t` with `pubnub_subscribe_message_type_t`, the discriminant on the raw subscribe event struct (see [The subscribe event struct](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#the-subscribe-event-struct)). `pubnub_event_type_t` is the field you read on a **history** result (`pubnub_history_message_result_t::event_type`, documented above). `pubnub_subscribe_message_type_t` is the field you read on a **subscribe** event (`pubnub_subscribe_event_t::type`). The two enums share similar-looking value names (`MESSAGE`, `SIGNAL`, `FILE`) and mostly the same numbers, but they are distinct types on distinct structs. Never pass one where the other is expected.

:::note Transparent decryption, with a narrower cache rule
When `pubnub_config_t::crypto_module` is configured (see [Provider pointers](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#provider-pointers)), `message` and file-message payloads decrypt automatically on access, so you never call a decrypt function yourself. Decryption failure is per-message, not per-call. If one message in a batch fails to decrypt, the fetch as a whole still succeeds: `message` keeps the original ciphertext instead of a parsed tree, and `crypto_result` is set to `PUBNUB_ERR_CRYPTO` so you can tell the two cases apart.
Check `crypto_result` rather than assuming a node in `message` is already plaintext. The decrypted payload is cached only until you access a **different** message or channel index, not only until `pubnub_future_release`, which is stricter than the usual "valid until release" rule. Copy out any bytes you need before advancing to the next index.
:::

### Other examples

#### Fetch messages with message actions

Read the actions tree with the same JSON-node pattern as `message`. The SDK does not provide typed accessors for individual action entries.

```c
#include <pubnub/client.h>
#include <pubnub/features/history.h>
#include <pubnub/future.h>
#include <pubnub/providers/serialization.h>

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

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id        = "my_unique_user_id";

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

    pubnub_fetch_messages_opts_t opts = PUBNUB_FETCH_MESSAGES_OPTS_INIT;
    opts.channels                = "ch1";
    opts.include_message_actions = 1;

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

    if (PUBNUB_OK == result) {
        pubnub_fetch_messages_result_t r = pubnub_fetch_messages_result(future);
        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_fetch_messages_channel_result_t ch =
                pubnub_fetch_messages_result_channel_at(future, i);
            for (size_t j = 0; j < ch.message_count; ++j) {
                const pubnub_json_value_t* actions =
                    pubnub_fetch_messages_result_actions_at(future, i, j);
                if (NULL != actions) {
                    printf("message %zu on channel %zu has actions\n", j, i);
                }
            }
        }
    }

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

#### Fetch messages, async callback

```c
#include <pubnub/client.h>
#include <pubnub/features/history.h>
#include <pubnub/future.h>
#include <pubnub/providers/serialization.h>
#include <pubnub/response.h>

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

static volatile int s_done = 0;

static void on_complete(pubnub_future_t future, pubnub_res_t status, void* user_data)
{
    pubnub_context_t* ctx = (pubnub_context_t*)user_data;

    if (PUBNUB_OK != status) {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("fetch failed: %s (%.*s)\n", pubnub_res_str(status), (int)err.len, err.ptr);
        pubnub_future_release(future);
        s_done = 1;
        return;
    }

    pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
    pubnub_fetch_messages_result_t   r       = pubnub_fetch_messages_result(future);

    for (size_t i = 0; i < r.channel_count; ++i) {
        pubnub_fetch_messages_channel_result_t ch =
            pubnub_fetch_messages_result_channel_at(future, i);
        for (size_t j = 0; j < ch.message_count; ++j) {
            pubnub_history_message_result_t msg =
                pubnub_fetch_messages_result_message_at(future, i, j);
            if (NULL != msg.message && NULL != serial && NULL != serial->value_as_string) {
                size_t      mlen = 0;
                const char* mptr = serial->value_as_string(msg.message, &mlen);
                if (NULL != mptr) {
                    printf("  %.*s: %.*s\n",
                           (int)msg.timetoken.len, msg.timetoken.ptr,
                           (int)mlen, mptr);
                }
            }
        }
    }

    pubnub_future_release(future);
    s_done = 1;
}

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id        = "my_unique_user_id";

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

    pubnub_fetch_messages_opts_t opts = PUBNUB_FETCH_MESSAGES_OPTS_INIT;
    opts.channels = "demo_channel";
    opts.count    = 25;

    pubnub_future_t future = pubnub_fetch_messages(ctx, &opts);

    pubnub_res_t rc = pubnub_async(future, on_complete, ctx);
    if (PUBNUB_OK != rc) {
        printf("pubnub_async failed: %s\n", pubnub_res_str(rc));
        pubnub_future_release(future);
        pubnub_destroy(ctx);
        return EXIT_FAILURE;
    }

    while (!s_done) {
        pubnub_process(ctx);
    }

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

Adapted from `examples/history/fetch_messages_async.c`. `on_complete` releases the future and reads its results itself, so `main` does not release it a second time.

#### Fetching further back (paging)

`pubnub_fetch_messages_result_t` carries a `next` field (`pubnub_timetoken_t`) alongside `channel_count`. When the server has more messages than it returned in one response, `next` is a non-empty cursor. Pass it as `start` on the next `pubnub_fetch_messages()` call to page further back. A zero-length `next` (`.len == 0`) means the current response is the last page.

```c
pubnub_fetch_messages_result_t r = pubnub_fetch_messages_result(future);
if (r.next.len > 0) {
    /* start requires a NUL-terminated string; next is a view and is not
       guaranteed to be one, so copy it into a buffer first. */
    char cursor[32];
    size_t len = (r.next.len < sizeof(cursor) - 1) ? r.next.len : sizeof(cursor) - 1;
    memcpy(cursor, r.next.ptr, len);
    cursor[len] = '\0';

    pubnub_fetch_messages_opts_t next_opts = PUBNUB_FETCH_MESSAGES_OPTS_INIT;
    next_opts.channels = "ch1,ch2";
    next_opts.start    = cursor;
}
```

You can also page manually by re-issuing the call with `start` set to the oldest `timetoken` you saw in the previous response. Because `start` is exclusive, this does not re-fetch the message at that boundary.

### Error responses

See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full `pubnub_res_t` catalog and the `pubnub_response_service_error()` pattern for server-side error detail. Specific to fetch: validation failures include a `NULL` `channels`, more than one channel combined with `include_message_actions`, a missing `subscribe_key`, or a full request queue. On any of these, the returned future carries an immediate error code, readable via `pubnub_future_status()` without polling or awaiting.

## Delete messages

`pubnub_delete_messages()` removes messages from a single channel's stored history.

### Method(s)

```c
pubnub_future_t pubnub_delete_messages(pubnub_context_t* ctx, const pubnub_delete_messages_opts_t* opts);
```

| Parameter | Description |
| --- | --- |
| `channel` *Type: `const char*`Default: — | Borrowed, NUL-terminated. **Single channel only** — commas are not allowed. |
| `start`Type: `const char*`Default: `NULL` (delete from the beginning of stored history) | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Exclusive.** Only messages published after `start` are deleted. The message at `start` itself is kept. |
| `end`Type: `const char*`Default: `NULL` (delete up to the most recent message) | Borrowed, NUL-terminated 17-digit decimal timetoken string. **Exclusive.** Only messages published before `end` are deleted. The message at `end` itself is kept. |
| `timeout_ms`Type: `uint32_t`Default: `0` (inherits context default) | A non-zero value overrides the context-level timeout for this call only. |

Zero-initialize with `PUBNUB_DELETE_MESSAGES_OPTS_INIT`, which expands to a plain `{0}`. Unlike fetch's options struct, there is no non-zero field to preserve here.

:::warning danger
Delete's
end
is exclusive — fetch's
end
is inclusive, and the old SDK's delete
end
was inclusive too
`pubnub_fetch_messages()`'s `end` is **inclusive**: the message at that timetoken is returned. `pubnub_delete_messages()`'s `end` is **exclusive**: the message at that timetoken is kept, not deleted. Code that assumes the two functions share one rule leaves exactly one message undeleted at the `end` boundary. The same happens if you port a delete call from the earlier C-Core SDK, where delete's `end` was inclusive. To delete a specific message, set `end` to a timetoken one greater than that message's own timetoken, or set `end` to the following message's timetoken.
:::

**C-family contract**

* **Header** — `#include <pubnub/features/history.h>`
* **Types** — `pubnub_delete_messages_opts_t`
* **Prerequisite** — an initialized context with `subscribe_key` set
* **Feature flag** — `PUBNUB_ENABLE_HISTORY`
* **Ownership / lifetime** — `channel`/`start`/`end` are borrowed, NUL-terminated strings
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

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

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

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id        = "my_unique_user_id";

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

    pubnub_delete_messages_opts_t opts = PUBNUB_DELETE_MESSAGES_OPTS_INIT;
    opts.channel = "my_channel";
    opts.end     = "17001234567890123";

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

    if (PUBNUB_OK == result) {
        printf("delete OK\n");
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("delete failed: %s (%.*s)\n", pubnub_res_str(result), (int)err.len, err.ptr);
    }

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

Adapted from `examples/history/delete_messages.c` (blocking `pubnub_await`).

### Returns

`pubnub_delete_messages()` has no result accessor of its own. Success is signaled by the future's status alone: check `pubnub_future_status(future) == PUBNUB_OK` (or the return value of `pubnub_await()`, as shown above). There is no data to extract on success or failure beyond the error detail described below.

### Error responses

See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full `pubnub_res_t` catalog. The header requires only that `subscribe_key` be set on the context. It states no other immediate-validation-error contract for this function, unlike fetch and message counts.

:::note Delete-From-History add-on
Deleting stored messages may also require the **Delete-From-History** setting enabled for your key in the [Admin Portal](https://admin.pubnub.com/). See [Message Persistence](https://www.pubnub.com/docs/general/storage.md). This SDK's `history.h` does not mention any additional requirement, such as a `secret_key`, for `pubnub_delete_messages()` to succeed. If your account still enforces one, it is a server-side/product requirement, not something the client header documents or enforces.
:::

## Message counts

`pubnub_message_counts()` counts messages published to one or more channels since a given timetoken, useful for showing an "unread" badge.

### Method(s)

```c
pubnub_future_t pubnub_message_counts(pubnub_context_t* ctx, const pubnub_message_counts_opts_t* opts);
```

| Parameter | Description |
| --- | --- |
| `channels` *Type: `const char*`Default: — | Borrowed, NUL-terminated. Comma-separated. |
| `timetoken`Type: `const char*`Default: — | Borrowed, NUL-terminated. A single timetoken applied to every channel in `channels`. Counts messages published after this timetoken. Mutually exclusive with `channels_timetokens`. |
| `channels_timetokens`Type: `const char*`Default: — | Borrowed, NUL-terminated. Comma-separated, one timetoken per channel, in the same order as `channels`. Mutually exclusive with `timetoken`. |
| `timeout_ms`Type: `uint32_t`Default: `0` (inherits context default) | A non-zero value overrides the context-level timeout for this call only. |

Exactly one of `timetoken` or `channels_timetokens` must be non-`NULL`. The header does not state whether the counted boundary is inclusive or exclusive of the supplied timetoken. Unlike fetch and delete, it uses neither word. Do not assume either behavior without confirming it against a live response.

Zero-initialize with `PUBNUB_MESSAGE_COUNTS_OPTS_INIT`, which expands to a plain `{0}`.

**C-family contract**

* **Header** — `#include <pubnub/features/history.h>`
* **Types** — `pubnub_message_counts_opts_t`, `pubnub_message_counts_result_t`, `pubnub_message_counts_channel_result_t`
* **Prerequisite** — an initialized context with `subscribe_key` set
* **Feature flag** — `PUBNUB_ENABLE_HISTORY`
* **Ownership / lifetime** — `channels`/`timetoken`/`channels_timetokens` are borrowed, NUL-terminated strings; result view fields alias memory owned by the future, valid until `pubnub_future_release`
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

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

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

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key = "demo";
    cfg.user_id        = "my_unique_user_id";

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

    pubnub_message_counts_opts_t opts = PUBNUB_MESSAGE_COUNTS_OPTS_INIT;
    opts.channels  = "ch1,ch2,ch3";
    opts.timetoken = "17001234567890123";

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

    if (PUBNUB_OK == result) {
        pubnub_message_counts_result_t r = pubnub_message_counts_result(future);
        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_message_counts_channel_result_t ch =
                pubnub_message_counts_result_channel_at(future, i);
            printf("[%.*s] unread: %u\n", (int)ch.name.len, ch.name.ptr, ch.count);
        }
    }
    else {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("counts failed: %s (%.*s)\n", pubnub_res_str(result), (int)err.len, err.ptr);
    }

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

Adapted from `examples/history/message_counts.c` (blocking `pubnub_await`).

### Returns

```c
pubnub_message_counts_result_t          pubnub_message_counts_result(pubnub_future_t future);
pubnub_message_counts_channel_result_t  pubnub_message_counts_result_channel_at(pubnub_future_t future, size_t index);
```

Call `pubnub_message_counts_result()` first to trigger lazy parsing, then loop with `pubnub_message_counts_result_channel_at()` up to `channel_count`. `pubnub_message_counts_channel_result_t` has `name` (`pubnub_string_view_t`) and `count` (`uint32_t`, the number of messages published after the supplied timetoken). As with fetch's channel accessor, **iteration order does not necessarily match the order channels were requested in.** Search by `name` rather than assuming an index. Every accessor returns a zero-initialized struct for an out-of-range index or a future that is not ready.

### Other examples

#### Per-channel timetokens

```c
pubnub_message_counts_opts_t opts = PUBNUB_MESSAGE_COUNTS_OPTS_INIT;
opts.channels           = "ch1,ch2,ch3";
opts.channels_timetokens = "17001111111111111,17001222222222222,17001333333333333";
```

### Error responses

See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full `pubnub_res_t` catalog. Setting both `timetoken` and `channels_timetokens`, or neither, fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

## 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.
* **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.
* **PubNub** - PubNub is a real-time messaging platform that provides APIs and SDKs for building scalable applications. It handles the complex infrastructure of real-time communication, including: Message delivery and persistence, Presence detection, Access control, Push notifications, File sharing, Serverless processing with Functions and Events & Actions, Analytics and monitoring with BizOps Workspace, AI-powered insights with Illuminate.
* **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
