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

# Presence 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.

Presence lets you track who is online, query and set custom per-user state on a channel, and observe join/leave/timeout activity as it happens. Learn more about the feature itself in the [Presence overview](https://www.pubnub.com/docs/general/presence/overview.md).

The C SDK exposes presence through two unrelated mechanisms, and the rest of this page is organized around that split:

1. **Request/response operations:** `pubnub_here_now()`, `pubnub_where_now()`, `pubnub_set_state()`, and `pubnub_get_state()`. Each call returns a `pubnub_future_t`, exactly like any other feature entry point (see [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md) for the shared async model).
2. **Pushed presence events:** join, leave, timeout, state-change, and interval notifications delivered through the subscribe listener's `on_presence` callback. These arrive only if the subscription was created with `with_presence` enabled.

A third topic, heartbeat, is not an operation at all. It has no callable function and no listener callback, and is driven entirely by configuration on `pubnub_config_t`, described in [Heartbeat](#heartbeat) below.

## Here now

:::warning Requires Presence
`pubnub_here_now()` requires that the Presence add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/).
:::

`pubnub_here_now()` returns the current occupancy of one or more channels: the connected user IDs (unless suppressed) and, optionally, each user's presence state.

### Method(s)

```c
pubnub_future_t pubnub_here_now(pubnub_context_t*             ctx,
                                 const pubnub_here_now_opts_t* opts);
```

`ctx` is the initialized context, borrowed for the call. `opts` is a pointer to the options below, also borrowed for the call.

`pubnub_here_now_opts_t` fields:

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channels | const | Optional | `none` | Comma-separated channel names, borrowed, NUL-terminated. |
| channel_groups | const | Optional | `NULL` | Comma-separated channel-group names, borrowed, NUL-terminated. |
| include_uuids | uint8_t | Optional | `1 (via PUBNUB_HERE_NOW_OPTS_INIT)` | Include each occupant's user ID in the response. |
| include_state | uint8_t | Optional | `0` | Include each occupant's presence state in the response. |
| limit | uint32_t | Optional | `0 → server default of 100, clamped to 1000` | Maximum number of occupants to return per channel. Setting `limit` above `0` enables occupant-level pagination alongside `offset`. |
| offset | uint32_t | Optional | `0` | Zero-based index of the first occupant to return. |
| timeout_ms | uint32_t | Optional | `0 → falls back to pubnub_config_t::transaction_timeout_ms` | Per-request timeout override. |

At least one of `channels` or `channel_groups` must be non-NULL and non-empty.

:::note No global here-now
A doc comment on `pubnub_here_now()` describes passing `NULL` for both `channels` and `channel_groups` to query occupancy across every channel. The implementation does not support this: leaving both fields empty returns `PUBNUB_ERR_INVALID_ARGUMENT`. Always supply at least one.
:::

**C-family contract**

* **Header** — `#include <pubnub/features/presence.h>`
* **Types** — `pubnub_here_now_opts_t`, `pubnub_here_now_result_t`, `pubnub_here_now_channel_result_t`, `pubnub_here_now_occupant_result_t`
* **Feature flag** — `PUBNUB_ENABLE_PRESENCE`
* **Ownership / lifetime** — `channels`/`channel_groups` are borrowed for the call only. Result string views (channel names, user IDs, state) stay valid until `pubnub_future_release()`.
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately. Consume it by cooperative polling, blocking await, or callback (see [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md)).

### Sample code

```c
#include "pubnub/pubnub.h"

#include <stdio.h>

int main(void)
{
    /* 1. Configure the client. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-here-now";

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

    /* 2. Query presence on a channel. */
    pubnub_here_now_opts_t opts = PUBNUB_HERE_NOW_OPTS_INIT;
    opts.channels               = "demo_channel";
    opts.include_state          = 1;

    pubnub_future_t fut = pubnub_here_now(ctx, &opts);

    /* 3. Drive I/O until the future is ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    /* 4. Read the result. */
    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_here_now_result_t r = pubnub_here_now_result(fut);
        printf("Total occupancy: %u\n", r.total_occupancy);

        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_here_now_channel_result_t ch =
                pubnub_here_now_result_channel_at(fut, i);
            printf("  %.*s: %u occupants\n", (int)ch.name.len, ch.name.ptr, ch.occupancy);

            for (size_t j = 0; j < ch.occupant_count; ++j) {
                pubnub_here_now_occupant_result_t occ =
                    pubnub_here_now_result_occupant_at(fut, i, j);
                printf("    uuid: %.*s", (int)occ.uuid.len, occ.uuid.ptr);
                if (NULL != occ.state.ptr) {
                    printf("  state: %.*s", (int)occ.state.len, occ.state.ptr);
                }
                printf("\n");
            }
        }
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("here_now failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 5. Cleanup. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

### Returns

`pubnub_here_now_result(future)` returns a `pubnub_here_now_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `total_occupancy` | `uint32_t` | Aggregate occupancy across every queried channel. |
| `total_channels` | `uint32_t` | Server-reported total channel count. |
| `channel_count` | `uint32_t` | Iteration bound for `pubnub_here_now_result_channel_at()`. |

Iterate channels with `pubnub_here_now_result_channel_at(future, index)` for `index` in `[0, channel_count)`, returning a `pubnub_here_now_channel_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `name` | `pubnub_string_view_t` | Channel name. |
| `occupancy` | `uint32_t` | Occupancy of this channel. |
| `occupant_count` | `uint32_t` | Iteration bound for `pubnub_here_now_result_occupant_at()`. |

Iterate occupants with `pubnub_here_now_result_occupant_at(future, ch_index, occ_index)` for `occ_index` in `[0, occupant_count)`, returning a `pubnub_here_now_occupant_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `uuid` | `pubnub_string_view_t` | Occupant's user ID. |
| `state` | `pubnub_string_view_t` | Raw JSON state text, or `{NULL,0}` when absent. |

:::note
This
state
is a string, not a parsed tree
`pubnub_here_now_occupant_result_t.state` is a raw `pubnub_string_view_t`: the JSON bytes as sent by the server, not a parsed node. This is different from `set_state`/`get_state` below, whose `state` fields are `const pubnub_json_value_t*` parsed trees. To read here-now state as structured data, parse it yourself with the serialization provider's `parse()` entry.
:::

All fields, and every indexed accessor, zero-initialize when the future is invalid, not ready, or carries an error, or when an index is out of range. Every string view returned by these accessors stays valid until `pubnub_future_release()` is called on the future. That is longer-lived than the presence-event fields covered in [Presence events](#presence-events) below.

### Error responses

Reading the future's status after it completes can produce:

| Result | Cause |
| --- | --- |
| `PUBNUB_ERR_INVALID_ARGUMENT` | `opts` is `NULL`; the context is invalid or uninitialized; or both `channels` and `channel_groups` are empty. |
| `PUBNUB_ERR_NOT_INITIALIZED` | The context has no allocator configured (should not occur in normal use). |
| `PUBNUB_ERR_OUT_OF_MEMORY` | Allocation failed while building the request. |

For the full result-value catalog and how to read a server-side error in detail, see [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md).

### Other examples

#### Async callback variant

All four presence request/response operations support the same async pattern: submit the call, register a completion callback with `pubnub_async()`, and read the result inside that callback instead of polling.

```c
#include "pubnub/pubnub.h"

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

static volatile int s_done;

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

    if (PUBNUB_OK == status) {
        pubnub_here_now_result_t r = pubnub_here_now_result(future);
        printf("  [callback] Total occupancy: %u\n", r.total_occupancy);
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(future);
        printf("  [callback] here_now 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-here-now-async";

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

    pubnub_here_now_opts_t opts = PUBNUB_HERE_NOW_OPTS_INIT;
    opts.channels               = "demo_channel";
    opts.include_state          = 1;

    pubnub_future_t fut = pubnub_here_now(ctx, &opts);

    pubnub_res_t rc = pubnub_async(fut, on_here_now_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;
    }

    /* On threaded platforms a background thread drives I/O and invokes
       the callback. On embedded targets without threads, call
       pubnub_process(ctx) in a loop instead of sleeping. */
    while (!s_done) {
        /* poll or sleep here, depending on your platform */
    }

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

## Where now

:::warning Requires Presence
`pubnub_where_now()` requires that the Presence add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/).
:::

`pubnub_where_now()` returns the list of channels a given user ID is currently present on.

:::note Timeout events
If the application restarts, or a device reconnects, within the heartbeat window, no timeout event fires for the previous session.
:::

### Method(s)

```c
pubnub_future_t pubnub_where_now(pubnub_context_t*              ctx,
                                  const pubnub_where_now_opts_t* opts);
```

`ctx` is the initialized context, borrowed for the call. `opts` is a pointer to the options below, also borrowed for the call.

`pubnub_where_now_opts_t` fields:

| Parameter | Description |
| --- | --- |
| `uuid`Type: `const char*`Default: `NULL` → the context's own `user_id` | User ID to look up, borrowed, NUL-terminated. |
| `timeout_ms`Type: `uint32_t`Default: `0` → falls back to `pubnub_config_t::transaction_timeout_ms` | Per-request timeout override. |

**C-family contract**

* **Header** — `#include <pubnub/features/presence.h>`
* **Types** — `pubnub_where_now_opts_t`, `pubnub_where_now_result_t`
* **Feature flag** — `PUBNUB_ENABLE_PRESENCE`
* **Ownership / lifetime** — `uuid` is borrowed for the call only. Result string views stay valid until `pubnub_future_release()`.
* **Blocking** — never blocks; same future model as [Here now](#here-now).

### Sample code

```c
#include "pubnub/pubnub.h"

#include <stdio.h>

int main(void)
{
    /* 1. Configure the client. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-where-now";

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

    /* 2. Query channels for a specific user (NULL = own user_id). */
    pubnub_where_now_opts_t opts = PUBNUB_WHERE_NOW_OPTS_INIT;
    opts.uuid                    = "target-user-42";

    pubnub_future_t fut = pubnub_where_now(ctx, &opts);

    /* 3. Drive I/O until the future is ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    /* 4. Read the result. */
    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_where_now_result_t r = pubnub_where_now_result(fut);
        printf("User is on %u channel(s):\n", r.channel_count);
        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_string_view_t ch = pubnub_where_now_result_channel_at(fut, i);
            printf("  %.*s\n", (int)ch.len, ch.ptr);
        }
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("where_now failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 5. Cleanup. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

### Returns

`pubnub_where_now_result(future)` returns a `pubnub_where_now_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `channel_count` | `uint32_t` | Number of channels the user is present on; iteration bound for `pubnub_where_now_result_channel_at()`. |

Iterate channels with `pubnub_where_now_result_channel_at(future, index)` for `index` in `[0, channel_count)`, returning a `pubnub_string_view_t` channel name. Zero-initialized (`{NULL,0}`) when out of range or the future is not ready. Valid until `pubnub_future_release()`.

### Error responses

Reading the future's status after it completes can produce:

| Result | Cause |
| --- | --- |
| `PUBNUB_ERR_INVALID_ARGUMENT` | `opts` is `NULL`, or the context is invalid or uninitialized. |
| `PUBNUB_ERR_NOT_INITIALIZED` | The context has no allocator configured. |

## User state

:::warning Requires Presence
`pubnub_set_state()` and `pubnub_get_state()` require that the Presence add-on is enabled for your key in the [Admin Portal](https://admin.pubnub.com/).
:::

A user can attach a custom JSON state object (score, typing status, location) to their presence on one or more channels. State is not persisted: once the client disconnects, the state is gone. See [Presence state](https://www.pubnub.com/docs/general/presence/presence-state.md) for the conceptual model.

### Set state

#### Method(s)

```c
pubnub_future_t pubnub_set_state(pubnub_context_t*              ctx,
                                  const pubnub_set_state_opts_t* opts);
```

`ctx` is the initialized context, borrowed for the call. `opts` is a pointer to the options below, also borrowed for the call.

`pubnub_set_state_opts_t` fields:

| Parameter | Description |
| --- | --- |
| `channels`Type: `const char*`Default: `NULL` | Comma-separated channel names, borrowed. |
| `channel_groups`Type: `const char*`Default: `NULL` | Comma-separated channel-group names, borrowed. |
| `state`Type: `const char*`Default: `NULL` | Raw JSON-object string, borrowed. |
| `state_len`Type: `size_t`Default: `0` → SDK calls `strlen(state)` | Length of `state` in bytes. |
| `state_value`Type: `pubnub_json_value_t*`Default: `NULL` | JSON value tree, borrowed during the call only. |
| `timeout_ms`Type: `uint32_t`Default: `0` | Per-request timeout override. |

At least one of `channels` or `channel_groups` is required.

:::warning danger
Set exactly one of
state
or
state_value
The header documents only that setting both `state` and `state_value` to non-NULL is an error. The implementation enforces more than that: leaving **both** fields `NULL` is rejected with `PUBNUB_ERR_INVALID_ARGUMENT` as well. Set exactly one, never zero and never two.
:::

**C-family contract**

* **Header** — `#include <pubnub/features/presence.h>`
* **Types** — `pubnub_set_state_opts_t`, `pubnub_set_state_result_t`, `pubnub_json_value_t`, `pubnub_serialization_provider_t`
* **Feature flag** — `PUBNUB_ENABLE_PRESENCE`
* **Ownership / lifetime** — `channels`/`channel_groups`/`state` are borrowed for the call only. If you use `state_value`, the SDK serializes the tree synchronously inside `pubnub_set_state()` before the call returns — you can call `pubnub_json_destroy()` on the tree immediately afterward, without waiting for the future to complete or be released.
* **Blocking** — never blocks; same future model as [Here now](#here-now).

#### Sample code: raw JSON string

```c
#include "pubnub/pubnub.h"

#include <stdio.h>

int main(void)
{
    /* 1. Configure the client. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-set-state";

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

    /* 2. Set presence state with a raw JSON string. */
    pubnub_set_state_opts_t opts = PUBNUB_SET_STATE_OPTS_INIT;
    opts.channels                = "demo_channel";
    opts.state                   = "{\"mood\":\"happy\",\"score\":42}";

    pubnub_future_t fut = pubnub_set_state(ctx, &opts);

    /* 3. Drive I/O until the future is ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    /* 4. Read the confirmed state. */
    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_set_state_result_t        r      = pubnub_set_state_result(fut);
        pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
        if (NULL != r.state && NULL != serial) {
            char buf[256];
            pubnub_json_to_debug_string(serial, r.state, buf, sizeof(buf));
            printf("State set: %s\n", buf);
        } else {
            printf("State set (no echo)\n");
        }
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("set_state failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 5. Cleanup. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

#### Sample code: JSON value tree

```c
#include "pubnub/pubnub.h"

#include "pubnub/json_macros.h"

#include <stdio.h>

int main(void)
{
    /* 1. Configure the client. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-set-state-value";

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

    /* 2. Build state as a JSON value tree. */
    pubnub_serialization_provider_t* json = pubnub_serialization(ctx);
    pubnub_json_value_t*             st =
        PUBNUB_JSON_OBJ(json,
                        PUBNUB_JSON_KV_STR(json, "mood", "happy"),
                        PUBNUB_JSON_KV_INT(json, "score", 42),
                        PUBNUB_JSON_KV_BOOL(json, "typing", 0));

    /* 3. Set presence state with the value tree. */
    pubnub_set_state_opts_t opts = PUBNUB_SET_STATE_OPTS_INIT;
    opts.channels                = "demo_channel";
    opts.state_value             = st;

    pubnub_future_t fut = pubnub_set_state(ctx, &opts);

    /* 4. Tree is borrowed during the call — safe to destroy now. */
    pubnub_json_destroy(json, st);

    /* 5. Drive I/O until the future is ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    /* 6. Read the confirmed state. */
    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_set_state_result_t r = pubnub_set_state_result(fut);
        if (NULL != r.state) {
            char buf[256];
            pubnub_json_to_debug_string(json, r.state, buf, sizeof(buf));
            printf("State set: %s\n", buf);
        } else {
            printf("State set (no echo)\n");
        }
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("set_state failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 7. Cleanup. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

#### Returns

`pubnub_set_state_result(future)` returns a `pubnub_set_state_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `state` | `const pubnub_json_value_t*` | Server-echoed state object, `NULL` when the server did not echo state. Valid until `pubnub_future_release()`. Walk it with the serialization provider's `object_get()`. |

`state` is a single aggregate object regardless of how many channels were targeted. There is no per-channel confirmed-state accessor for `set_state`.

#### Error responses

| Result | Cause |
| --- | --- |
| `PUBNUB_ERR_INVALID_ARGUMENT` | `opts` is `NULL`; the context is invalid; both `channels`/`channel_groups` are empty; or `state`/`state_value` are not set to exactly one. |
| `PUBNUB_ERR_PROVIDER_MISSING` | The serialization provider, or its `serialize` entry, is unavailable. |
| `PUBNUB_ERR_SERIALIZATION` | Serialization of `state_value` produced zero bytes. |
| `PUBNUB_ERR_OUT_OF_MEMORY` | Allocation failed while building the request. |

### Get state

#### Method(s)

```c
pubnub_future_t pubnub_get_state(pubnub_context_t*              ctx,
                                  const pubnub_get_state_opts_t* opts);
```

`ctx` is the initialized context, borrowed for the call. `opts` is a pointer to the options below, also borrowed for the call.

`pubnub_get_state_opts_t` fields:

| Parameter | Description |
| --- | --- |
| `channels`Type: `const char*`Default: `NULL` | Comma-separated channel names, borrowed. |
| `channel_groups`Type: `const char*`Default: `NULL` | Comma-separated channel-group names, borrowed. |
| `uuid`Type: `const char*`Default: `NULL` → the context's own `user_id` | User ID to query, borrowed. |
| `timeout_ms`Type: `uint32_t`Default: `0` | Per-request timeout override. |

At least one of `channels` or `channel_groups` is required.

**C-family contract**

* **Header** — `#include <pubnub/features/presence.h>`
* **Types** — `pubnub_get_state_opts_t`, `pubnub_get_state_result_t`, `pubnub_get_state_channel_result_t`
* **Feature flag** — `PUBNUB_ENABLE_PRESENCE`
* **Ownership / lifetime** — `channels`/`channel_groups`/`uuid` are borrowed for the call only. Result views and state pointers stay valid until `pubnub_future_release()`.
* **Blocking** — never blocks; same future model as [Here now](#here-now).

#### Sample code

```c
#include "pubnub/pubnub.h"

#include <stdio.h>

int main(void)
{
    /* 1. Configure the client. */
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "demo";
    cfg.user_id         = "example-get-state";

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

    /* 2. Get state for a user on multiple channels. */
    pubnub_get_state_opts_t opts = PUBNUB_GET_STATE_OPTS_INIT;
    opts.channels                = "ch1,ch2";
    opts.uuid                    = "target-user-42";

    pubnub_future_t fut = pubnub_get_state(ctx, &opts);

    /* 3. Drive I/O until the future is ready. */
    while (!pubnub_future_is_ready(fut)) {
        pubnub_process(ctx);
    }

    /* 4. Read the per-channel state. */
    const pubnub_res_t status = pubnub_future_status(fut);
    if (PUBNUB_OK == status) {
        pubnub_get_state_result_t        r      = pubnub_get_state_result(fut);
        pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
        for (size_t i = 0; i < r.channel_count; ++i) {
            pubnub_get_state_channel_result_t entry =
                pubnub_get_state_result_channel_at(fut, i);
            printf("  %.*s => ", (int)entry.channel.len, entry.channel.ptr);
            if (NULL != entry.state) {
                char buf[256];
                pubnub_json_to_debug_string(serial, entry.state, buf, sizeof(buf));
                printf("%s", buf);
            } else {
                printf("(none)");
            }
            printf("\n");
        }
    } else {
        pubnub_string_view_t err = pubnub_response_error_message(fut);
        printf("get_state failed: %.*s\n", (int)err.len, err.ptr);
    }

    /* 5. Cleanup. */
    pubnub_future_release(fut);
    pubnub_destroy(ctx);
    return 0;
}
```

#### Returns

`pubnub_get_state_result(future)` returns a `pubnub_get_state_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `channel_count` | `uint32_t` | Number of channels with state entries; iteration bound for `pubnub_get_state_result_channel_at()`. |

Iterate with `pubnub_get_state_result_channel_at(future, index)` for `index` in `[0, channel_count)`, returning a `pubnub_get_state_channel_result_t`:

| Field | Type | Description |
| --- | --- | --- |
| `channel` | `pubnub_string_view_t` | Channel name. |
| `state` | `const pubnub_json_value_t*` | User's state on this channel, `NULL` when no state is set. Walk with the serialization provider's `object_get()`. |

#### Error responses

| Result | Cause |
| --- | --- |
| `PUBNUB_ERR_INVALID_ARGUMENT` | `opts` is `NULL`; the context is invalid; or both `channels`/`channel_groups` are empty. |
| `PUBNUB_ERR_NOT_INITIALIZED` | The context has no allocator configured. |

## Presence events

Presence events are pushed to your subscribe listener as they happen, independently of the request/response operations above. General listener registration, including the `pubnub_add_listener()` pattern and thread/callback context for global listeners, is covered in [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md) and [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md). This section covers only what is presence-specific.

To receive presence events on a channel, create its subscription with `with_presence` set on `pubnub_subscription_opts_t`:

```c
static void subscribe_with_presence(pubnub_context_t* ctx)
{
    pubnub_entity_t entity = pubnub_channel(ctx, "demo_channel");
    pubnub_subscription_t sub =
        pubnub_subscription_create(entity, &(pubnub_subscription_opts_t){ .with_presence = 1 });
    pubnub_entity_destroy(entity);
    pubnub_subscription_subscribe(sub);
}
```

`with_presence` also subscribes to the channel's implicit `<channel>-pnpres` presence channel. It is silently ignored when the entity is a channel-metadata or user-metadata object.

Register an `on_presence` callback on a `pubnub_subscribe_listener_t`, then call `pubnub_subscribe_event_presence()` inside it to decode the generic subscribe event into a typed `pubnub_subscribe_presence_event_t`. The callback itself receives a `const pubnub_subscribe_event_t*`. See [The subscribe event struct](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#the-subscribe-event-struct) for its fields and the full list of discriminant values.

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>` and `#include <pubnub/features/subscribe_types.h>`
* **Types** — `pubnub_subscribe_presence_event_t`, `pubnub_presence_action_t`, `pubnub_subscribe_presence_cb_t`
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`; presence events are available whenever subscribe is compiled in, independent of `PUBNUB_ENABLE_PRESENCE`.
* **Thread / callback context** — same as every other subscribe listener callback: fires inside `pubnub_process()` in cooperative mode, or on the background thread on threaded builds (`PUBNUB_CFG_THREAD_SAFETY=1`), where I/O is driven automatically without any additional call. Keep callbacks fast and non-reentrant. See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full model.

`pubnub_subscribe_presence_event_t` fields:

| Field | Type | Populated for | Description |
| --- | --- | --- | --- |
| `action` | `pubnub_presence_action_t` | always | `PUBNUB_PRESENCE_JOIN`, `_LEAVE`, `_TIMEOUT`, `_STATE_CHANGE`, `_INTERVAL`. |
| `uuid` | `pubnub_string_view_t` | join/leave/timeout/state-change | May be empty for interval events. |
| `channel` | `pubnub_string_view_t` | always | Channel the event occurred on. |
| `subscription` | `pubnub_string_view_t` | always | Wildcard or channel-group match pattern. |
| `occupancy` | `uint32_t` | always | Current occupancy count. |
| `timetoken` | `pubnub_string_view_t` | always | Publish timetoken of the event. |
| `state` | `const pubnub_json_value_t*` | when carrying state | `NULL` when absent. JSON-node pointer, walk with the serialization provider's `object_get()`. |
| `joined` | `const pubnub_json_value_t*` | `PUBNUB_PRESENCE_INTERVAL` only | JSON array of user IDs that joined since the last interval; `NULL` when the action is not interval, or when the server-side delta feature is unavailable. |
| `left` | `const pubnub_json_value_t*` | `PUBNUB_PRESENCE_INTERVAL` only | Same shape as `joined`, for users that left. |
| `timed_out` | `const pubnub_json_value_t*` | `PUBNUB_PRESENCE_INTERVAL` only | Same shape as `joined`, for users that timed out. |
| `here_now_refresh` | `uint8_t` | `PUBNUB_PRESENCE_INTERVAL` | `1` = the `joined`/`left`/`timed_out` arrays were omitted because the payload exceeded roughly 30 KB. Call [Here now](#here-now) for the full occupant list. `0` = the arrays are present (subject to the delta-availability note above). |

:::note Shorter-lived than the request/response results
Every field in `pubnub_subscribe_presence_event_t` is valid only for the duration of the listener callback. That is strictly shorter-lived than the here-now/where-now/set-state/get-state results above, which stay valid until `pubnub_future_release()`. Copy any bytes you need before the callback returns. Never retain these pointers.
:::

:::note No public control over interval deltas
No configuration field, option, or function in the public headers enables or disables the `joined`/`left`/`timed_out` delta arrays. Their availability on `PUBNUB_PRESENCE_INTERVAL` events depends on a server-side setting the client cannot currently control. Always check these pointers for `NULL` before use, and treat `here_now_refresh` as the authoritative signal for whether the arrays are complete.
:::

### Sample code

```c
#include "pubnub/pubnub.h"

#include <stdio.h>

static void print_uuid_array(pubnub_serialization_provider_t* serial,
                              const pubnub_json_value_t*       arr,
                              const char*                      label)
{
    if (NULL == arr || NULL == serial || NULL == serial->array_size
        || NULL == serial->array_get || NULL == serial->value_as_string) {
        return;
    }

    size_t n = serial->array_size(arr);
    for (size_t i = 0; i < n; ++i) {
        pubnub_json_value_t* item = serial->array_get(arr, i);
        size_t                len  = 0;
        const char*           uuid = serial->value_as_string(item, &len);
        if (NULL != uuid) {
            printf("  %s: %.*s\n", label, (int)len, uuid);
        }
    }
}

static void on_presence(const pubnub_subscribe_event_t* event, void* user_data)
{
    pubnub_context_t* ctx = (pubnub_context_t*)user_data;

    pubnub_subscribe_presence_event_t pres;
    if (PUBNUB_OK != pubnub_subscribe_event_presence(ctx, event, &pres)) {
        return;
    }

    printf("action=%d channel=%.*s occupancy=%u\n",
           (int)pres.action,
           (int)pres.channel.len,
           pres.channel.ptr,
           pres.occupancy);

    if (PUBNUB_PRESENCE_INTERVAL == pres.action) {
        if (pres.here_now_refresh) {
            printf("delta arrays omitted; call pubnub_here_now for the full list\n");
        } else {
            pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
            print_uuid_array(serial, pres.joined, "joined");
            print_uuid_array(serial, pres.left, "left");
            print_uuid_array(serial, pres.timed_out, "timed_out");
        }
    } else {
        printf("uuid=%.*s\n", (int)pres.uuid.len, pres.uuid.ptr);
    }
}
```

Register it, together with an entity subscription created with `with_presence = 1`, using `pubnub_add_listener()` as shown in [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md).

### Returns

`pubnub_subscribe_event_presence(ctx, event, out)` returns a `pubnub_res_t`:

| Result | Cause |
| --- | --- |
| `PUBNUB_OK` | `out` populated successfully. |
| `PUBNUB_ERR_INVALID_ARGUMENT` | `ctx`, `event`, or `out` is `NULL`, or `event`'s type is not presence. |
| `PUBNUB_ERR_SERIALIZATION` | The event payload failed to parse. |

## Heartbeat

Heartbeat has no callable function and no listener callback. There is no `pubnub_heartbeat()`, and no event struct reports individual heartbeat outcomes. It is entirely driven by three fields on `pubnub_config_t`: `presence_timeout`, `heartbeat_interval`, and `suppress_leave_events`. Their types and defaults are covered in [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#presence). This section covers only their presence-specific behavior.

:::warning Recurring heartbeat is off unless you set heartbeat_interval
`heartbeat_interval` at `0` (its default, and what `pubnub_config_defaults()` leaves it at) disables the recurring client-side heartbeat. The SDK doesn't derive an interval from `presence_timeout`. When a subscription joins, the SDK still sends one initial heartbeat to announce the client, but no timer repeats it, so the server times the client out after `presence_timeout` seconds.
To keep the client present, set `heartbeat_interval` to a non-zero number of seconds, shorter than `presence_timeout`. `presence_timeout` only sets the server-side `heartbeat=` value (the SDK uses 300 seconds when it's `0`). It isn't a client-side scheduling fallback.
:::

Once heartbeat is configured, no separate call starts it: creating and activating a subscription (`pubnub_subscription_create()` followed by `pubnub_subscription_subscribe()`) is enough. The SDK's internal presence manager sends heartbeat requests automatically every `heartbeat_interval` seconds for as long as the subscription stays active, alongside the subscribe long-poll itself. There is no way to observe individual heartbeat successes or failures from application code. They surface only indirectly, through the subscription's own status events (see [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md)).

`suppress_leave_events` controls only whether the SDK sends an explicit leave request when a subscription ends. When set, the server relies solely on `presence_timeout` expiry to detect the client's departure.

## 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.

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