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

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

App Context provides serverless storage for metadata about your application's users and channels, plus the membership relationships between them: which channels a user belongs to, and which users belong to a channel. All 12 App Context functions live on a single header, `#include <pubnub/features/app_context.h>`, gated by one compile-time flag, `PUBNUB_ENABLE_APP_CONTEXT`. Unlike [Entities](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#entities), no App Context function takes an entity handle. Every function takes a context plus plain string identifiers instead.

The two result structs that describe a relationship each embed the other side's metadata struct directly: a `pubnub_membership_t` (a user's view of a channel) embeds a full `pubnub_channel_metadata_t` in its `channel` field, and a `pubnub_member_t` (a channel's view of a user) embeds a full `pubnub_uuid_metadata_t` in its `uuid` field. Keep this in mind when reading the field tables below. The [Memberships](#memberships) and [Channel members](#channel-members) sections do not repeat the metadata field tables already given under [UUID metadata](#uuid-metadata) and [Channel metadata](#channel-metadata).

:::warning custom_value ownership TRANSFERS to the SDK: the opposite of publish, signal, and presence
Every `custom_value` field in this header (`pubnub_set_uuid_metadata_opts_t.custom_value`, `pubnub_set_channel_metadata_opts_t.custom_value`, and `custom_value` inside `pubnub_membership_input_t`/`pubnub_member_input_t`) works differently from every other JSON-tree field in this SDK. **The SDK consumes and frees the tree during the call, on both success and failure.** Do not access or free it afterward. Doing so is a double free, and no diagnostic catches it for you.
This is the **opposite** of `publish.h`'s `message_value`, `signal.h`'s `message_value`, and `presence.h`'s `state_value`, which are all **borrowed**: the caller retains ownership and must destroy them with `pubnub_json_destroy()` after the call returns. If you learned the borrowed pattern from [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md) or [Presence](https://www.pubnub.com/docs/sdks/c/api-reference/presence.md) and apply it here, you will double-free the tree. App Context's `custom_value` is the sole outlier across all four headers.
:::

## Include flags

Every App Context list, get, and set-with-page operation accepts an `include` field of type `uint32_t`, a bitwise OR of the flags below. Pass `0` to get whichever fields the server returns by default. Combining flags costs nothing extra: `opts.include = PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;`.

| Flag | What it adds | Applies to |
| --- | --- | --- |
| `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` | The entity's own `custom` JSON object | Any single UUID or channel metadata result |
| `PUBNUB_APP_CONTEXT_INCLUDE_TYPE` | The entity's own `type` label | Any single UUID or channel metadata result |
| `PUBNUB_APP_CONTEXT_INCLUDE_STATUS` | The entity's own `status` label | Any single UUID or channel metadata result |
| `PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT` | Populates `pubnub_app_context_page_t.total_count`; otherwise it stays `0` | Every paginated list/set-with-page operation |
| `PUBNUB_APP_CONTEXT_INCLUDE_UUID` | Populates the embedded `pubnub_uuid_metadata_t` inside a `pubnub_member_t` result | `pubnub_get_channel_members`, `pubnub_set_channel_members` |
| `PUBNUB_APP_CONTEXT_INCLUDE_UUID_CUSTOM` | Adds `custom` to that embedded UUID metadata | Same |
| `PUBNUB_APP_CONTEXT_INCLUDE_UUID_TYPE` | Adds `type` to that embedded UUID metadata | Same |
| `PUBNUB_APP_CONTEXT_INCLUDE_UUID_STATUS` | Adds `status` to that embedded UUID metadata | Same |
| `PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL` | Populates the embedded `pubnub_channel_metadata_t` inside a `pubnub_membership_t` result | `pubnub_get_memberships`, `pubnub_set_memberships` |
| `PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL_CUSTOM` | Adds `custom` to that embedded channel metadata | Same |
| `PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL_TYPE` | Adds `type` to that embedded channel metadata | Same |
| `PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL_STATUS` | Adds `status` to that embedded channel metadata | Same |

Without the base `PUBNUB_APP_CONTEXT_INCLUDE_UUID`/`_CHANNEL` flag set, whether the corresponding `_UUID_*`/`_CHANNEL_*` sub-flags have any independent effect is not documented. Do not rely on a sub-flag alone. Always set the base flag first when you need the embedded struct populated.

`pubnub_membership_t.channel` and `pubnub_member_t.uuid` are documented as partial unless the matching include flag is set. The header does not enumerate a specific default subset of fields for the unflagged case, so do not assume which fields (if any) come back.

## Pagination

Every list operation (`pubnub_get_all_uuid_metadata`, `pubnub_get_all_channel_metadata`, `pubnub_get_memberships`, `pubnub_get_channel_members`) and every set-with-page operation (`pubnub_set_memberships`, `pubnub_set_channel_members`) returns a `pubnub_app_context_page_t`:

```c
typedef struct pubnub_app_context_page {
    uint32_t total_count;
    pubnub_string_view_t next;
    pubnub_string_view_t prev;
    uint32_t count;
} pubnub_app_context_page_t;
```

This is a real cursor, unlike [Message Persistence history](https://www.pubnub.com/docs/sdks/c/api-reference/storage-and-playback.md), which has none.

* **count** is the number of items actually returned in this response. Read it, not the `limit` you requested, to know how many results to iterate.
* **total_count** is only populated when `PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT` is set. Otherwise it stays `0`.
* **Advancing forward**: pass the previous response's `next` cursor as the next call's `start` field.
* **Going backward**: pass the previous response's `prev` cursor as the next call's `end` field.
* **next.len == 0 means there are no more pages.**
* **Page size** is `opts.limit` (`uint32_t`). `0` lets the server pick its own default ("typically 100" per the header's doc comments). **No client-side maximum is enforced.** Whatever you set in `limit` passes straight through to the request unclamped.
* **Sorting**: `opts.sort`, a comma-separated `field:direction` string passed verbatim (for example, `"name:asc,updated:desc"` or, for memberships, `"channel.name:asc"`). No enumerated list of valid field names exists in the header. Treat it as an opaque server-side contract.
* **Filtering**: `opts.filter`, passed verbatim as the server-side filter expression (for example, `"status == \"active\""`). No filter-expression grammar is documented in this SDK's source.

Every paginated `_result()` accessor returns a zero-initialized `pubnub_app_context_page_t` (`count == 0`) if the future is not ready or invalid. Check `pubnub_future_status()` to tell an empty result apart from an error. Every `_at()` accessor (for example, `pubnub_get_all_uuid_metadata_result_uuid_at()`) returns a zero-initialized struct, silently, for an out-of-range `index`. There is no bounds-checking error code, so always compare `index < page.count` yourself before calling one.

All string views returned by any App Context result, including every field inside `pubnub_app_context_page_t`, alias the parsed response body and stay valid only until `pubnub_future_release()` is called on the owning future. Read everything you need before releasing it.

## Custom metadata

Every set-style App Context operation accepts custom data two mutually exclusive ways:

1. **A raw JSON string.** `custom` (`const char*`) plus `custom_len` (`size_t`, `0` calls `strlen()`). Must be a valid JSON object. **Borrowed.** The SDK does not take ownership, so there is no cleanup obligation beyond the string's normal lifetime.
2. **A pre-built JSON value tree.** `custom_value` (`struct pubnub_json_value*`), built with the `pubnub/json.h`/`pubnub/json_macros.h` helpers. **Ownership transfers to the SDK.** See the warning at the top of this page: the SDK consumes and frees this tree during the call, on both success and failure. Never call `pubnub_json_destroy()` on it afterward.

Setting both `custom` and `custom_value` non-NULL on the same struct (or the same array element, for memberships/members) fails immediately with `PUBNUB_ERR_INVALID_ARGUMENT`. This is readable from `pubnub_future_status()` without polling, since the check runs before any network I/O. Setting neither is not an error: `custom` is simply omitted from the request, preserving whatever is already stored.

:::warning Unsupported partial updates of custom metadata
The value of the custom metadata parameter sent in this method always overwrites the value stored on PubNub servers. If you want to add new custom data to an existing one, you must:
1. Get the existing metadata and store it locally.
2. Append the new custom metadata to the existing one.
3. Set the entire updated custom object.
:::

### Reading custom data back

Every `custom` field in an App Context result (`pubnub_uuid_metadata_t.custom`, `pubnub_channel_metadata_t.custom`, and the `custom` field inside `pubnub_membership_t`/`pubnub_member_t`) is `const struct pubnub_json_value*`, an opaque JSON-tree node, not a string. Read it with the same pattern already established for subscribe payloads in [Receiving messages](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#typed-event-extractors): get the context's serialization provider with `pubnub_serialization(ctx)` (see [Accessing the serialization provider](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#accessing-the-serialization-provider)), NULL-check the specific vtable accessor you need, then read.

```c
static void print_custom_role(pubnub_context_t* ctx, const pubnub_json_value_t* custom)
{
    if (NULL == custom) {
        return;
    }

    pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
    if (NULL == serial || NULL == serial->object_get || NULL == serial->value_as_string) {
        return;
    }

    const pubnub_json_value_t* role = serial->object_get(custom, "role", 4);
    if (NULL == role) {
        return;
    }

    size_t      len = 0;
    const char* val = serial->value_as_string(role, &len);
    if (NULL != val) {
        printf("role: %.*s\n", (int)len, val);
    }
}
```

For a quick debug print of an entire node instead of walking individual keys, use the gated helper `pubnub_json_to_debug_string(serial, node, buf, sizeof(buf))` (`PUBNUB_CFG_JSON_HELPERS`), shown in the [Set UUID metadata](#set-uuid-metadata) sample below.

## UUID metadata

### Get all UUID metadata

Returns a paginated list of UUID metadata objects.

#### Method(s)

```c
pubnub_future_t pubnub_get_all_uuid_metadata(
    pubnub_context_t* ctx, const pubnub_get_all_uuid_metadata_opts_t* opts);
```

`opts` may be `NULL`; the SDK substitutes an all-defaults struct. Zero-initialize an explicit struct with `PUBNUB_GET_ALL_UUID_METADATA_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `include`Type: `uint32_t`Default: `0` | See [Include flags](#include-flags). |
| `limit`Type: `uint32_t`Default: `0` (server default) | Items per page. See [Pagination](#pagination). |
| `start`Type: `const char*`Default: `NULL` | Next-page cursor, borrowed, NUL-terminated. |
| `end`Type: `const char*`Default: `NULL` | Previous-page cursor, borrowed, NUL-terminated. |
| `filter`Type: `const char*`Default: `NULL` | Server-side filter expression. |
| `sort`Type: `const char*`Default: `NULL` | Comma-separated `field:direction` pairs. |
| `timeout_ms`Type: `uint32_t`Default: `0` (context default) | Per-request override. |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Types** — `pubnub_get_all_uuid_metadata_opts_t`, `pubnub_app_context_page_t`, `pubnub_uuid_metadata_t`
* **Prerequisite** — an initialized context
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Ownership / lifetime** — all string fields borrowed; no JSON-tree fields on this struct
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

#### Sample code

Adapted from `examples/app_context/get_all_uuid_metadata.c`.

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

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

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

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

    pubnub_get_all_uuid_metadata_opts_t opts = PUBNUB_GET_ALL_UUID_METADATA_OPTS_INIT;
    opts.include = PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;
    opts.filter  = "status == \"active\"";
    opts.limit   = 25;

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_get_all_uuid_metadata_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_uuid_metadata_t m = pubnub_get_all_uuid_metadata_result_uuid_at(future, i);
            printf("uuid: %.*s\n", (int)m.id.len, m.id.ptr);
        }
        if (0 != page.next.len) {
            printf("more pages available\n");
        }
    }
    else {
        printf("get_all_uuid_metadata failed: %d\n", (int)result);
    }

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

#### Returns

```c
pubnub_app_context_page_t pubnub_get_all_uuid_metadata_result(pubnub_future_t future);
pubnub_uuid_metadata_t    pubnub_get_all_uuid_metadata_result_uuid_at(pubnub_future_t future, size_t index);
```

Call both accessors only after the future completes with `PUBNUB_OK`, and before releasing the future. See [Pagination](#pagination) for the bounds and lifetime rules that apply to every list result on this page.

```c
typedef struct pubnub_uuid_metadata {
    pubnub_string_view_t id;
    pubnub_string_view_t name;
    pubnub_string_view_t external_id;
    pubnub_string_view_t profile_url;
    pubnub_string_view_t email;
    pubnub_string_view_t type;
    pubnub_string_view_t status;
    const struct pubnub_json_value* custom;
    pubnub_string_view_t updated;
    pubnub_string_view_t etag;
} pubnub_uuid_metadata_t;
```

#### Other examples

##### Walk multiple pages

```c
static void walk_all_uuid_metadata(pubnub_context_t* ctx)
{
    char next_buf[128] = { 0 };

    pubnub_get_all_uuid_metadata_opts_t opts = PUBNUB_GET_ALL_UUID_METADATA_OPTS_INIT;
    opts.include = PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;
    opts.limit   = 50;

    for (;;) {
        pubnub_future_t future = pubnub_get_all_uuid_metadata(ctx, &opts);
        pubnub_res_t     result = pubnub_await(future);
        if (PUBNUB_OK != result) {
            pubnub_future_release(future);
            break;
        }

        pubnub_app_context_page_t page = pubnub_get_all_uuid_metadata_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_uuid_metadata_t m = pubnub_get_all_uuid_metadata_result_uuid_at(future, i);
            printf("uuid: %.*s\n", (int)m.id.len, m.id.ptr);
        }

        int no_more_pages = (0 == page.next.len);
        if (!no_more_pages) {
            size_t len = page.next.len < sizeof(next_buf) ? page.next.len : sizeof(next_buf) - 1;
            memcpy(next_buf, page.next.ptr, len);
            next_buf[len] = '\0';
            opts.start = next_buf;
        }

        pubnub_future_release(future);

        if (no_more_pages) {
            break;
        }
    }
}
```

### Get UUID metadata

Returns metadata for a single UUID.

#### Method(s)

```c
pubnub_future_t pubnub_get_uuid_metadata(
    pubnub_context_t* ctx, const pubnub_get_uuid_metadata_opts_t* opts);
```

`opts` may be `NULL`. Zero-initialize an explicit struct with `PUBNUB_GET_UUID_METADATA_OPTS_INIT`. Unlike most App Context options macros, this one is not a flat `{0}`: it sets `include` to `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` by default.

| Parameter | Description |
| --- | --- |
| `uuid`Type: `const char*`Default: `NULL` = the context's own `user_id` | Borrowed, NUL-terminated. |
| `include`Type: `uint32_t`Default: `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` via `PUBNUB_GET_UUID_METADATA_OPTS_INIT`, `0` if you zero the struct by hand | See [Include flags](#include-flags). |
| `timeout_ms`Type: `uint32_t`Default: `0` | Per-request override. |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Types** — `pubnub_get_uuid_metadata_opts_t`, `pubnub_uuid_metadata_t`
* **Prerequisite** — an initialized context; if `uuid` is omitted, `user_id` must be set
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/get_uuid_metadata.c`.

```c
static void get_uuid_metadata_example(pubnub_context_t* ctx)
{
    pubnub_get_uuid_metadata_opts_t opts = PUBNUB_GET_UUID_METADATA_OPTS_INIT;
    opts.uuid    = "my_unique_user_id";

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

    if (PUBNUB_OK == result) {
        pubnub_uuid_metadata_t meta = pubnub_get_uuid_metadata_result(future);
        printf("name: %.*s\n", (int)meta.name.len, meta.name.ptr);
    }
    else {
        printf("get_uuid_metadata failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_uuid_metadata_t pubnub_get_uuid_metadata_result(pubnub_future_t future);
```

Returns a zero-initialized struct if the future is not ready. Fields not covered by `include` are also zero-initialized.

### Set UUID metadata

Creates or partially updates a UUID's metadata. `opts` is required. Passing `NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

#### Method(s)

```c
pubnub_future_t pubnub_set_uuid_metadata(
    pubnub_context_t* ctx, const pubnub_set_uuid_metadata_opts_t* opts);
```

Zero-initialize with `PUBNUB_SET_UUID_METADATA_OPTS_INIT`. Like `PUBNUB_GET_UUID_METADATA_OPTS_INIT`, this defaults `include` to `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` rather than `0`.

| Parameter | Ownership | Description |
| --- | --- | --- |
| `uuid`Type: `const char*`Default: `NULL` = context's own `user_id` | Borrowed |  |
| `name`Type: `const char*`Default: `NULL` = unchanged | Borrowed | Partial PATCH. |
| `external_id`Type: `const char*`Default: `NULL` | Borrowed |  |
| `profile_url`Type: `const char*`Default: `NULL` | Borrowed |  |
| `email`Type: `const char*`Default: `NULL` | Borrowed |  |
| `type`Type: `const char*`Default: `NULL` | Borrowed |  |
| `status`Type: `const char*`Default: `NULL` | Borrowed |  |
| `custom`Type: `const char*`Default: `NULL` | Borrowed | Raw JSON string. See [Custom metadata](#custom-metadata). |
| `custom_len`Type: `size_t`Default: `0` = `strlen(custom)` | — |  |
| `custom_value`Type: `struct pubnub_json_value*`Default: `NULL` | **Transfers to the SDK** | See the warning at the top of this page. Setting both `custom` and `custom_value` is `PUBNUB_ERR_INVALID_ARGUMENT`. |
| `include`Type: `uint32_t`Default: `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` via `PUBNUB_SET_UUID_METADATA_OPTS_INIT`, `0` if zeroed by hand |  | See [Include flags](#include-flags). |
| `if_match`Type: `const char*`Default: `NULL` = unconditional | Borrowed, must stay valid until the function returns | Sent as an `If-Match` header. The server rejects the update if the current ETag does not match; no specific `pubnub_res_t` is documented for that case. |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Types** — `pubnub_set_uuid_metadata_opts_t`, `pubnub_uuid_metadata_t`
* **Prerequisite** — an initialized context; `opts` non-`NULL`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Ownership / lifetime** — `custom`/other strings borrowed; `custom_value`, if set, is consumed and freed by the SDK during the call
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/set_uuid_metadata.c`. Uses the raw-string `custom` field, read back with `pubnub_json_to_debug_string()`.

```c
#include <pubnub/client.h>
#include <pubnub/features/app_context.h>
#include <pubnub/future.h>
#include <pubnub/json.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.publish_key   = "demo";
    cfg.user_id       = "my_unique_user_id";

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

    pubnub_set_uuid_metadata_opts_t opts = PUBNUB_SET_UUID_METADATA_OPTS_INIT;
    opts.name    = "John Doe";
    opts.custom  = "{\"role\":\"developer\",\"level\":42}";

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

    if (PUBNUB_OK == result) {
        pubnub_uuid_metadata_t meta = pubnub_set_uuid_metadata_result(future);

        pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
        if (NULL != meta.custom && NULL != serial) {
            char   buf[256];
            size_t written = pubnub_json_to_debug_string(serial, meta.custom, buf, sizeof(buf));
            if (written > 0) {
                printf("custom: %.*s\n", (int)written, buf);
            }
        }
    }
    else {
        printf("set_uuid_metadata failed: %d\n", (int)result);
    }

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

#### Returns

```c
pubnub_uuid_metadata_t pubnub_set_uuid_metadata_result(pubnub_future_t future);
```

#### Other examples

##### Build a custom_value JSON tree

This is exactly the case the ownership warning above exists for: notice there is no `pubnub_json_destroy()` call anywhere in this sample, on either the success or the failure path.

```c
#include <pubnub/client.h>
#include <pubnub/features/app_context.h>
#include <pubnub/future.h>
#include <pubnub/json.h>
#include <pubnub/json_macros.h>

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

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

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

    pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
    if (NULL == serial) {
        pubnub_destroy(ctx);
        return EXIT_FAILURE;
    }

    pubnub_json_value_t* tree = PUBNUB_JSON_OBJ(serial,
        PUBNUB_JSON_KV_STR(serial, "role", "developer"),
        PUBNUB_JSON_KV_INT(serial, "level", 42));

    pubnub_res_t result = PUBNUB_ERR_INTERNAL;
    if (NULL != tree) {
        pubnub_set_uuid_metadata_opts_t opts = PUBNUB_SET_UUID_METADATA_OPTS_INIT;
        opts.custom_value = tree;

        pubnub_future_t future = pubnub_set_uuid_metadata(ctx, &opts);
        result = pubnub_await(future);
        pubnub_future_release(future);

        /* Do NOT call pubnub_json_destroy() on tree here. custom_value's
           ownership TRANSFERS to the SDK: it consumes and frees the tree
           during the call, on both success and failure. Freeing it again
           would be a double free. */
    }

    pubnub_destroy(ctx);
    return (PUBNUB_OK == result) ? EXIT_SUCCESS : EXIT_FAILURE;
}
```

##### Conditional update with if_match

```c
static void set_uuid_metadata_if_match(pubnub_context_t* ctx, const pubnub_uuid_metadata_t* current)
{
    char   etag_buf[128];
    size_t len = current->etag.len < sizeof(etag_buf) ? current->etag.len : sizeof(etag_buf) - 1;
    memcpy(etag_buf, current->etag.ptr, len);
    etag_buf[len] = '\0';

    pubnub_set_uuid_metadata_opts_t opts = PUBNUB_SET_UUID_METADATA_OPTS_INIT;
    opts.status   = "active";
    opts.if_match = etag_buf;

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

    if (PUBNUB_OK != result) {
        /* An ETag mismatch surfaces as a server-side rejection; no
           specific pubnub_res_t is documented for this case. */
        printf("set_uuid_metadata (if_match) failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Error responses

* Setting both `custom` and `custom_value` fails immediately with `PUBNUB_ERR_INVALID_ARGUMENT`.
* Passing `opts == NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.
* See [Error handling](#error-handling) for the universal `pubnub_res_t` catalog.

### Remove UUID metadata

#### Method(s)

```c
pubnub_future_t pubnub_remove_uuid_metadata(
    pubnub_context_t* ctx, const pubnub_remove_uuid_metadata_opts_t* opts);
```

`opts` may be `NULL`. This removes the context's own `user_id`. Zero-initialize an explicit struct with `PUBNUB_REMOVE_UUID_METADATA_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `uuid`Type: `const char*`Default: `NULL` = context's own `user_id` | Borrowed. |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Types** — `pubnub_remove_uuid_metadata_opts_t`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/remove_uuid_metadata.c`.

```c
static void remove_uuid_metadata_example(pubnub_context_t* ctx)
{
    pubnub_remove_uuid_metadata_opts_t opts = PUBNUB_REMOVE_UUID_METADATA_OPTS_INIT;
    opts.uuid = "my_unique_user_id";

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

    printf("remove_uuid_metadata: %s\n", (PUBNUB_OK == result) ? "ok" : "failed");

    pubnub_future_release(future);
}
```

#### Returns

**No result accessor exists for this operation.** Success is determined solely from the `pubnub_res_t` returned by `pubnub_future_status()`/`pubnub_await()`: `PUBNUB_OK` on success, an error code otherwise.

## Channel metadata

### Get all channel metadata

Same shape as [Get all UUID metadata](#get-all-uuid-metadata), without a `uuid` field. It lists every channel.

#### Method(s)

```c
pubnub_future_t pubnub_get_all_channel_metadata(
    pubnub_context_t* ctx, const pubnub_get_all_channel_metadata_opts_t* opts);
```

`opts` may be `NULL`. Zero-initialize an explicit struct with `PUBNUB_GET_ALL_CHANNEL_METADATA_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `include`Type: `uint32_t`Default: `0` | See [Include flags](#include-flags). |
| `limit`Type: `uint32_t`Default: `0` (server default) | See [Pagination](#pagination). |
| `start`Type: `const char*`Default: `NULL` | Borrowed. |
| `end`Type: `const char*`Default: `NULL` | Borrowed. |
| `filter`Type: `const char*`Default: `NULL` |  |
| `sort`Type: `const char*`Default: `NULL` |  |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Types** — `pubnub_get_all_channel_metadata_opts_t`, `pubnub_app_context_page_t`, `pubnub_channel_metadata_t`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/get_all_channel_metadata.c`.

```c
static void get_all_channel_metadata_example(pubnub_context_t* ctx)
{
    pubnub_get_all_channel_metadata_opts_t opts = PUBNUB_GET_ALL_CHANNEL_METADATA_OPTS_INIT;
    opts.include = PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;
    opts.limit   = 25;

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_get_all_channel_metadata_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_channel_metadata_t m = pubnub_get_all_channel_metadata_result_channel_at(future, i);
            printf("channel: %.*s\n", (int)m.id.len, m.id.ptr);
        }
    }
    else {
        printf("get_all_channel_metadata failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_app_context_page_t pubnub_get_all_channel_metadata_result(pubnub_future_t future);
pubnub_channel_metadata_t pubnub_get_all_channel_metadata_result_channel_at(pubnub_future_t future, size_t index);
```

```c
typedef struct pubnub_channel_metadata {
    pubnub_string_view_t id;
    pubnub_string_view_t name;
    pubnub_string_view_t description;
    pubnub_string_view_t type;
    pubnub_string_view_t status;
    const struct pubnub_json_value* custom;
    pubnub_string_view_t updated;
    pubnub_string_view_t etag;
} pubnub_channel_metadata_t;
```

### Get channel metadata

#### Method(s)

```c
pubnub_future_t pubnub_get_channel_metadata(
    pubnub_context_t* ctx, const pubnub_get_channel_metadata_opts_t* opts);
```

`opts` and `opts->channel` are both **required**. Either being absent fails with `PUBNUB_ERR_INVALID_ARGUMENT`. Zero-initialize with `PUBNUB_GET_CHANNEL_METADATA_OPTS_INIT`, which defaults `include` to `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` rather than `0`.

| Parameter | Description |
| --- | --- |
| `channel`Type: `const char*`Default: — | Borrowed, NUL-terminated. |
| `include`Type: `uint32_t`Default: `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` via `PUBNUB_GET_CHANNEL_METADATA_OPTS_INIT` |  |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/get_channel_metadata.c`.

```c
static void get_channel_metadata_example(pubnub_context_t* ctx)
{
    pubnub_get_channel_metadata_opts_t opts = PUBNUB_GET_CHANNEL_METADATA_OPTS_INIT;
    opts.channel = "my_channel";

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

    if (PUBNUB_OK == result) {
        pubnub_channel_metadata_t meta = pubnub_get_channel_metadata_result(future);
        printf("name: %.*s\n", (int)meta.name.len, meta.name.ptr);
    }
    else {
        printf("get_channel_metadata failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_channel_metadata_t pubnub_get_channel_metadata_result(pubnub_future_t future);
```

#### Error responses

`opts == NULL` or `opts->channel == NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

### Set channel metadata

`opts` and `opts->channel` are both **required**.

#### Method(s)

```c
pubnub_future_t pubnub_set_channel_metadata(
    pubnub_context_t* ctx, const pubnub_set_channel_metadata_opts_t* opts);
```

Zero-initialize with `PUBNUB_SET_CHANNEL_METADATA_OPTS_INIT`, which defaults `include` to `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` rather than `0`.

| Parameter | Ownership | Description |
| --- | --- | --- |
| `channel`Type: `const char*`Default: — | Borrowed |  |
| `name`Type: `const char*`Default: `NULL` | Borrowed |  |
| `description`Type: `const char*`Default: `NULL` | Borrowed |  |
| `type`Type: `const char*`Default: `NULL` | Borrowed |  |
| `status`Type: `const char*`Default: `NULL` | Borrowed |  |
| `custom`Type: `const char*`Default: `NULL` | Borrowed | See [Custom metadata](#custom-metadata). |
| `custom_len`Type: `size_t`Default: `0` = strlen | — |  |
| `custom_value`Type: `struct pubnub_json_value*`Default: `NULL` | **Transfers to the SDK** | Same rule as [Set UUID metadata](#set-uuid-metadata). |
| `include`Type: `uint32_t`Default: `PUBNUB_APP_CONTEXT_INCLUDE_CUSTOM` via `PUBNUB_SET_CHANNEL_METADATA_OPTS_INIT` |  |  |
| `if_match`Type: `const char*`Default: `NULL` | Borrowed, valid until return | Sent as `If-Match`. |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Ownership / lifetime** — same as [Set UUID metadata](#set-uuid-metadata): strings borrowed, `custom_value` transfers
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/set_channel_metadata.c`.

```c
static void set_channel_metadata_example(pubnub_context_t* ctx)
{
    pubnub_set_channel_metadata_opts_t opts = PUBNUB_SET_CHANNEL_METADATA_OPTS_INIT;
    opts.channel     = "my_channel";
    opts.name        = "Support Channel";
    opts.description = "Customer support";
    opts.custom      = "{\"topic\":\"support\"}";

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

    if (PUBNUB_OK != result) {
        printf("set_channel_metadata failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_channel_metadata_t pubnub_set_channel_metadata_result(pubnub_future_t future);
```

#### Error responses

Same as [Set UUID metadata](#set-uuid-metadata): both `custom` and `custom_value` set, or a missing `channel`, both fail with `PUBNUB_ERR_INVALID_ARGUMENT`.

### Remove channel metadata

#### Method(s)

```c
pubnub_future_t pubnub_remove_channel_metadata(
    pubnub_context_t* ctx, const pubnub_remove_channel_metadata_opts_t* opts);
```

`opts` and `opts->channel` are both **required**. Unlike UUID metadata, there is no context-level fallback for a channel. Zero-initialize an explicit struct with `PUBNUB_REMOVE_CHANNEL_METADATA_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `channel`Type: `const char*`Default: — | Borrowed. |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/remove_channel_metadata.c`.

```c
static void remove_channel_metadata_example(pubnub_context_t* ctx)
{
    pubnub_remove_channel_metadata_opts_t opts = PUBNUB_REMOVE_CHANNEL_METADATA_OPTS_INIT;
    opts.channel = "my_channel";

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

    printf("remove_channel_metadata: %s\n", (PUBNUB_OK == result) ? "ok" : "failed");

    pubnub_future_release(future);
}
```

#### Returns

**No result accessor exists for this operation**, the same pattern as [Remove UUID metadata](#remove-uuid-metadata). Check the `pubnub_res_t` only.

## Memberships

A membership is a UUID's view of its channel relationships: `pubnub_membership_t.channel` embeds the full `pubnub_channel_metadata_t` documented under [Channel metadata](#channel-metadata).

```c
typedef struct pubnub_membership {
    pubnub_channel_metadata_t channel;
    pubnub_string_view_t status;
    pubnub_string_view_t type;
    const struct pubnub_json_value* custom;
    pubnub_string_view_t updated;
    pubnub_string_view_t etag;
} pubnub_membership_t;
```

`channel` is populated only as far as the `PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL*` flags request. See [Include flags](#include-flags).

### Get memberships

Lists the channels a UUID belongs to.

#### Method(s)

```c
pubnub_future_t pubnub_get_memberships(
    pubnub_context_t* ctx, const pubnub_get_memberships_opts_t* opts);
```

`opts` may be `NULL`. This queries the context's own `user_id`. Zero-initialize an explicit struct with `PUBNUB_GET_MEMBERSHIPS_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `uuid`Type: `const char*`Default: `NULL` = context's own `user_id` | Borrowed. |
| `include`Type: `uint32_t`Default: `0` | See [Include flags](#include-flags). |
| `limit`Type: `uint32_t`Default: `0` (server default) | See [Pagination](#pagination). |
| `start`Type: `const char*`Default: `NULL` |  |
| `end`Type: `const char*`Default: `NULL` |  |
| `filter`Type: `const char*`Default: `NULL` |  |
| `sort`Type: `const char*`Default: `NULL` | For example, `"channel.name:asc"`. |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/get_memberships.c`.

```c
static void get_memberships_example(pubnub_context_t* ctx)
{
    pubnub_get_memberships_opts_t opts = PUBNUB_GET_MEMBERSHIPS_OPTS_INIT;
    opts.include = PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL
                  | PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL_CUSTOM
                  | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;
    opts.sort = "channel.name:asc";

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_get_memberships_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_membership_t ms = pubnub_get_memberships_result_membership_at(future, i);
            printf("channel: %.*s\n", (int)ms.channel.id.len, ms.channel.id.ptr);
        }
    }
    else {
        printf("get_memberships failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_app_context_page_t pubnub_get_memberships_result(pubnub_future_t future);
pubnub_membership_t       pubnub_get_memberships_result_membership_at(pubnub_future_t future, size_t index);
```

### Set memberships

Adds and/or removes channel memberships for a UUID in a single call. `opts` is **required**. The header documents that a call where **both** `set_count` and `remove_count` are zero fails with `PUBNUB_ERR_INVALID_ARGUMENT` (a no-op call), so build every call with at least one non-empty array. There is no `if_match` field on this struct: conditional update is not available for membership set operations.

#### Method(s)

```c
pubnub_future_t pubnub_set_memberships(
    pubnub_context_t* ctx, const pubnub_set_memberships_opts_t* opts);
```

| Parameter | Description |
| --- | --- |
| `uuid`Type: `const char*`Default: `NULL` = context's own `user_id` | Borrowed. |
| `set`Type: `const pubnub_membership_input_t*`Default: `NULL` = no additions | Borrowed array. |
| `set_count`Type: `size_t`Default: `0` | Element count of `set`. |
| `remove`Type: `const pubnub_membership_input_t*`Default: `NULL` = no removals | Borrowed array. Only `channel_id` is read for a removal; the rest of each element is ignored. |
| `remove_count`Type: `size_t`Default: `0` | Element count of `remove`. |
| `include`Type: `uint32_t`Default: `0` |  |
| `limit`Type: `uint32_t`Default: `0` | Applies to the **response** page, not the `set`/`remove` arrays. |
| `start` / `end` / `filter` / `sort`Type: `const char*`Default: `NULL` | Same as [Get memberships](#get-memberships). |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

`pubnub_membership_input_t`, used for each element of `set`:

```c
typedef struct pubnub_membership_input {
    const char* channel_id;
    const char* status;
    const char* type;
    const char* custom;
    size_t custom_len;
    struct pubnub_json_value* custom_value;
} pubnub_membership_input_t;
```

`channel_id` is required per element. `custom`/`custom_value` follow the same two-mode, mutual-exclusion, ownership-transfer rule described in [Custom metadata](#custom-metadata), applied per element rather than once for the whole call.

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Ownership / lifetime** — `set`/`remove` arrays borrowed; each element's `custom_value`, if set, transfers to the SDK
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

`examples/app_context/get_memberships.c` only reads. The shipped "set" example under this area, `set_members.c`, demonstrates the channel-side `pubnub_set_channel_members` instead (see [Set channel members](#set-channel-members)). This sample follows the same shape for the membership side.

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

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

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

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

    pubnub_membership_input_t to_set[1] = { { 0 } };
    to_set[0].channel_id = "channel_a";
    to_set[0].custom     = "{\"role\":\"member\"}";

    pubnub_membership_input_t to_remove[1] = { { 0 } };
    to_remove[0].channel_id = "channel_b"; /* only channel_id is read for a removal */

    pubnub_set_memberships_opts_t opts = PUBNUB_SET_MEMBERSHIPS_OPTS_INIT;
    opts.set          = to_set;
    opts.set_count    = 1;
    opts.remove       = to_remove;
    opts.remove_count = 1;
    opts.include      = PUBNUB_APP_CONTEXT_INCLUDE_CHANNEL | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_set_memberships_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_membership_t ms = pubnub_set_memberships_result_membership_at(future, i);
            printf("channel: %.*s\n", (int)ms.channel.id.len, ms.channel.id.ptr);
        }
    }
    else {
        printf("set_memberships failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
    pubnub_destroy(ctx);
    return (PUBNUB_OK == result) ? EXIT_SUCCESS : EXIT_FAILURE;
}
```

#### Returns

```c
pubnub_app_context_page_t pubnub_set_memberships_result(pubnub_future_t future);
pubnub_membership_t       pubnub_set_memberships_result_membership_at(pubnub_future_t future, size_t index);
```

#### Error responses

* `opts == NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.
* `set_count == 0 && remove_count == 0` fails with `PUBNUB_ERR_INVALID_ARGUMENT` (a no-op call). This is documented in the header's own doc comment.
* Setting both `custom` and `custom_value` on the same array element fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

## Channel members

A member is a channel's view of its UUID relationships: `pubnub_member_t.uuid` embeds the full `pubnub_uuid_metadata_t` documented under [UUID metadata](#uuid-metadata).

```c
typedef struct pubnub_member {
    pubnub_uuid_metadata_t uuid;
    pubnub_string_view_t status;
    pubnub_string_view_t type;
    const struct pubnub_json_value* custom;
    pubnub_string_view_t updated;
    pubnub_string_view_t etag;
} pubnub_member_t;
```

`uuid` is populated only as far as the `PUBNUB_APP_CONTEXT_INCLUDE_UUID*` flags request. See [Include flags](#include-flags).

### Get channel members

Lists the UUIDs that belong to a channel. `opts` and `opts->channel` are both **required**.

#### Method(s)

```c
pubnub_future_t pubnub_get_channel_members(
    pubnub_context_t* ctx, const pubnub_get_channel_members_opts_t* opts);
```

Zero-initialize an explicit struct with `PUBNUB_GET_CHANNEL_MEMBERS_OPTS_INIT` (expands to `{0}`).

| Parameter | Description |
| --- | --- |
| `channel`Type: `const char*`Default: — | Borrowed. |
| `include`Type: `uint32_t`Default: `0` |  |
| `limit`Type: `uint32_t`Default: `0` (server default) |  |
| `start` / `end` / `filter` / `sort`Type: `const char*`Default: `NULL` | Same as [Get memberships](#get-memberships). |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/get_channel_members.c`.

```c
static void get_channel_members_example(pubnub_context_t* ctx)
{
    pubnub_get_channel_members_opts_t opts = PUBNUB_GET_CHANNEL_MEMBERS_OPTS_INIT;
    opts.channel = "my_channel";
    opts.include = PUBNUB_APP_CONTEXT_INCLUDE_UUID | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_get_channel_members_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_member_t mem = pubnub_get_channel_members_result_member_at(future, i);
            printf("uuid: %.*s\n", (int)mem.uuid.id.len, mem.uuid.id.ptr);
        }
    }
    else {
        printf("get_channel_members failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
}
```

#### Returns

```c
pubnub_app_context_page_t pubnub_get_channel_members_result(pubnub_future_t future);
pubnub_member_t           pubnub_get_channel_members_result_member_at(pubnub_future_t future, size_t index);
```

#### Error responses

`opts == NULL` or `opts->channel == NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

### Set channel members

Adds and/or removes UUIDs from a channel's member list in a single call. `opts` and `opts->channel` are **required**. Exactly like [Set memberships](#set-memberships), the header documents that a call where **both** `set_count` and `remove_count` are zero fails with `PUBNUB_ERR_INVALID_ARGUMENT` (a no-op call). There is no `if_match` field on this struct either.

#### Method(s)

```c
pubnub_future_t pubnub_set_channel_members(
    pubnub_context_t* ctx, const pubnub_set_channel_members_opts_t* opts);
```

| Parameter | Description |
| --- | --- |
| `channel`Type: `const char*`Default: — | Borrowed. |
| `set`Type: `const pubnub_member_input_t*`Default: `NULL` | Borrowed array. |
| `set_count`Type: `size_t`Default: `0` |  |
| `remove`Type: `const pubnub_member_input_t*`Default: `NULL` | Borrowed array. Only `uuid_id` is read for a removal. |
| `remove_count`Type: `size_t`Default: `0` |  |
| `include`Type: `uint32_t`Default: `0` |  |
| `limit`Type: `uint32_t`Default: `0` | Response page size. |
| `start` / `end` / `filter` / `sort`Type: `const char*`Default: `NULL` |  |
| `timeout_ms`Type: `uint32_t`Default: `0` |  |

`pubnub_member_input_t`, used for each element of `set`:

```c
typedef struct pubnub_member_input {
    const char* uuid_id;
    const char* status;
    const char* type;
    const char* custom;
    size_t custom_len;
    struct pubnub_json_value* custom_value;
} pubnub_member_input_t;
```

`uuid_id` is required per element. `custom`/`custom_value` follow the same rule as [Custom metadata](#custom-metadata), per element.

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Ownership / lifetime** — `set`/`remove` arrays borrowed; each element's `custom_value`, if set, transfers to the SDK
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT`
* **Blocking** — never blocks

#### Sample code

Adapted from `examples/app_context/set_members.c`.

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

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

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

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

    pubnub_member_input_t to_set[1] = { { 0 } };
    to_set[0].uuid_id = "my_unique_user_id";
    to_set[0].custom  = "{\"role\":\"moderator\"}";

    pubnub_set_channel_members_opts_t opts = PUBNUB_SET_CHANNEL_MEMBERS_OPTS_INIT;
    opts.channel   = "my_channel";
    opts.set       = to_set;
    opts.set_count = 1;
    opts.include   = PUBNUB_APP_CONTEXT_INCLUDE_UUID | PUBNUB_APP_CONTEXT_INCLUDE_TOTAL_COUNT;

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

    if (PUBNUB_OK == result) {
        pubnub_app_context_page_t page = pubnub_set_channel_members_result(future);
        for (uint32_t i = 0; i < page.count; i++) {
            pubnub_member_t mem = pubnub_set_channel_members_result_member_at(future, i);
            printf("uuid: %.*s\n", (int)mem.uuid.id.len, mem.uuid.id.ptr);
        }
    }
    else {
        printf("set_channel_members failed: %d\n", (int)result);
    }

    pubnub_future_release(future);
    pubnub_destroy(ctx);
    return (PUBNUB_OK == result) ? EXIT_SUCCESS : EXIT_FAILURE;
}
```

#### Returns

```c
pubnub_app_context_page_t pubnub_set_channel_members_result(pubnub_future_t future);
pubnub_member_t           pubnub_set_channel_members_result_member_at(pubnub_future_t future, size_t index);
```

#### Error responses

* `opts == NULL` or `opts->channel == NULL` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.
* `set_count == 0 && remove_count == 0` fails with `PUBNUB_ERR_INVALID_ARGUMENT`.
* Setting both `custom` and `custom_value` on the same array element fails with `PUBNUB_ERR_INVALID_ARGUMENT`.

## Receiving App Context events

For the generic listener, subscription, and entity plumbing this section assumes, see [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#listeners). App Context change notifications arrive through the same `pubnub_subscribe_event_t` delivered to a listener's `on_app_context` callback as every other subscribe event type.

### App context event extractor

#### Method(s)

```c
pubnub_res_t pubnub_subscribe_event_app_context(
    pubnub_context_t* ctx,
    const pubnub_subscribe_event_t* event,
    pubnub_subscribe_app_context_event_t* out);
```

```c
typedef enum pubnub_app_context_event_type {
    PUBNUB_APP_CONTEXT_SET = 0,
    PUBNUB_APP_CONTEXT_REMOVED = 1
} pubnub_app_context_event_type_t;

typedef enum pubnub_app_context_object_type {
    PUBNUB_APP_CONTEXT_OBJECT_UNKNOWN = 0,
    PUBNUB_APP_CONTEXT_OBJECT_UUID = 1,
    PUBNUB_APP_CONTEXT_OBJECT_CHANNEL = 2,
    PUBNUB_APP_CONTEXT_OBJECT_MEMBERSHIP = 3
} pubnub_app_context_object_type_t;

typedef struct pubnub_subscribe_app_context_event {
    pubnub_app_context_event_type_t event;
    pubnub_app_context_object_type_t object_type;
    pubnub_string_view_t channel;
    pubnub_string_view_t subscription;
    const struct pubnub_json_value* data;
} pubnub_subscribe_app_context_event_t;
```

`pubnub_subscribe_event_app_context()` is declared in `#include <pubnub/features/subscribe.h>`, not `app_context.h`. The two enums and the event struct above are declared in `#include <pubnub/features/subscribe_types.h>` (included transitively by `subscribe.h`), not in `app_context.h` either. `app_context.h` only forward-declares `struct pubnub_subscribe_app_context_event` (for the typed sub-accessor signatures in [Typed sub-accessors](#typed-sub-accessors) below) and is not required to call this extractor on its own.

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>` (declares the function; pulls in `subscribe_types.h`, which declares the enums and event struct)
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`
* **Blocking** — never blocks; called synchronously inside a listener callback

`object_type` is an **enum**, not a string view. Switch on it rather than treating it like `channel`/`subscription`. Reading `object_type.len`/`.ptr` does not compile.

#### Sample code

This follows the same pattern as `examples/subscribe/callback.c`'s `on_app_context` handler: switch on the enum, then read `channel`/`subscription` as string views.

```c
#include <pubnub/client.h>
#include <pubnub/features/app_context.h>
#include <pubnub/features/subscribe.h>
#include <pubnub/features/subscribe_types.h>

#include <stdio.h>

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

    pubnub_subscribe_app_context_event_t obj;
    if (PUBNUB_OK != pubnub_subscribe_event_app_context(ctx, event, &obj)) {
        return;
    }

    const char* type_str;
    switch (obj.object_type) {
        case PUBNUB_APP_CONTEXT_OBJECT_UUID:       type_str = "uuid"; break;
        case PUBNUB_APP_CONTEXT_OBJECT_CHANNEL:    type_str = "channel"; break;
        case PUBNUB_APP_CONTEXT_OBJECT_MEMBERSHIP: type_str = "membership"; break;
        default:                                   type_str = "unknown"; break;
    }
    printf("app_context %s event on %.*s\n", type_str, (int)obj.channel.len, obj.channel.ptr);
}
```

#### Returns

`PUBNUB_OK` on success. `PUBNUB_ERR_INVALID_ARGUMENT` if any argument is `NULL` or the event is not an App Context event. `PUBNUB_ERR_SERIALIZATION` on parse failure. `obj.data`, when non-`NULL`, is a JSON-tree node. Read it with the pattern in [Reading custom data back](#reading-custom-data-back), or use one of the typed sub-accessors below instead of walking it manually.

### Typed sub-accessors

Three functions parse `obj.data` from the extractor above into the same typed metadata structs used by the request/response operations on this page. **Gated by both PUBNUB_ENABLE_APP_CONTEXT and PUBNUB_ENABLE_SUBSCRIBE.**

The SDK's own `on_app_context` handler in `examples/subscribe/callback.c` calls the generic extractor above and walks `obj.data` manually. The typed sub-accessors below do that parsing for you when you already know which App Context object type you expect.

#### Method(s)

```c
pubnub_res_t pubnub_subscribe_app_context_uuid_metadata(
    pubnub_context_t* ctx,
    const struct pubnub_subscribe_app_context_event* event,
    pubnub_uuid_metadata_t* out);

pubnub_res_t pubnub_subscribe_app_context_channel_metadata(
    pubnub_context_t* ctx,
    const struct pubnub_subscribe_app_context_event* event,
    pubnub_channel_metadata_t* out);

pubnub_res_t pubnub_subscribe_app_context_membership(
    pubnub_context_t* ctx,
    const struct pubnub_subscribe_app_context_event* event,
    pubnub_membership_t* out);
```

Call the one matching `event->object_type` (`PUBNUB_APP_CONTEXT_OBJECT_UUID`/`_CHANNEL`/`_MEMBERSHIP`).

**C-family contract**

* **Header** — `#include <pubnub/features/app_context.h>`
* **Feature flag** — `PUBNUB_ENABLE_APP_CONTEXT` **and** `PUBNUB_ENABLE_SUBSCRIBE`
* **Ownership / lifetime** — `out` is caller-provided; every field inside it is valid only for the duration of the listener callback that produced `event`. This is shorter than the lifetime of REST results on this page, which stay valid until you call `pubnub_future_release()`. Copy anything you need before the callback returns.
* **Blocking** — never blocks

#### Sample code

Combining the three accessors with the extractor above:

```c
#include <pubnub/client.h>
#include <pubnub/features/app_context.h>
#include <pubnub/features/subscribe.h>
#include <pubnub/features/subscribe_types.h>

#include <stdio.h>

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

    pubnub_subscribe_app_context_event_t obj;
    if (PUBNUB_OK != pubnub_subscribe_event_app_context(ctx, event, &obj)) {
        return;
    }

    switch (obj.object_type) {
    case PUBNUB_APP_CONTEXT_OBJECT_UUID: {
        pubnub_uuid_metadata_t uuid_meta;
        if (PUBNUB_OK == pubnub_subscribe_app_context_uuid_metadata(ctx, &obj, &uuid_meta)) {
            printf("uuid metadata changed: %.*s\n", (int)uuid_meta.id.len, uuid_meta.id.ptr);
        }
        break;
    }
    case PUBNUB_APP_CONTEXT_OBJECT_CHANNEL: {
        pubnub_channel_metadata_t channel_meta;
        if (PUBNUB_OK == pubnub_subscribe_app_context_channel_metadata(ctx, &obj, &channel_meta)) {
            printf("channel metadata changed: %.*s\n", (int)channel_meta.id.len, channel_meta.id.ptr);
        }
        break;
    }
    case PUBNUB_APP_CONTEXT_OBJECT_MEMBERSHIP: {
        pubnub_membership_t membership;
        if (PUBNUB_OK == pubnub_subscribe_app_context_membership(ctx, &obj, &membership)) {
            printf("membership changed on channel: %.*s\n",
                   (int)membership.channel.id.len, membership.channel.id.ptr);
        }
        break;
    }
    default:
        printf("app_context event on unknown object type\n");
        break;
    }
}
```

#### Returns

`PUBNUB_OK` on success, including when `event->data == NULL`, in which case `*out` is left zero-initialized. `PUBNUB_ERR_INVALID_ARGUMENT` if `ctx`, `event`, or `out` is `NULL`. `PUBNUB_ERR_SERIALIZATION` if the context's serialization provider is unavailable.

## Error handling

`pubnub_res_t` is the single status type shared across this SDK. App Context has no per-function error accessors. See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full result-code catalog and the `pubnub_response_service_error()` pattern for server-side error detail.

Two validation errors are specific to this feature area and worth keeping close at hand:

* **Setting both custom and custom_value** non-`NULL` on the same struct (or the same `set` array element) is `PUBNUB_ERR_INVALID_ARGUMENT`, checked before any network I/O.
* **pubnub_set_memberships/pubnub_set_channel_members with set_count == 0 && remove_count == 0** is `PUBNUB_ERR_INVALID_ARGUMENT` (a no-op call), as documented on each function's own doc comment.

## 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.
* **User** - An individual or entity that interacts with a system, application, or service. In PubNub, a user typically refers to someone who sends or receives messages through the platform, identified by a unique user ID or username.

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