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

# Publish/Subscribe 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.

Publish and signal are single-shot, asynchronous request/response operations. Subscribe is a long-lived streaming operation built on four cooperating concepts: entities (channel/channel-group/metadata handles), subscriptions (an activation of one entity), subscription sets (a group of subscriptions managed together), and listeners (callback structs that receive events). There is exactly one subscribe model in this SDK, with no legacy variant to choose between.

For higher-level conceptual details, refer to [Connection Management](https://www.pubnub.com/docs/general/setup/connection-management.md) and [Publish Messages](https://www.pubnub.com/docs/general/messages/publish.md).

The typical lifecycle, in order, is: create an entity → create a subscription from it → register listeners → activate the subscription → read events in your listener callbacks → unsubscribe → destroy the subscription → remove the listener → destroy the context. Destroying an entity is safe as soon as a subscription has been created from it. The two do not need to be destroyed together. Getting the rest of this order wrong is the most common resource-management mistake on this page, so each sample below shows a complete, correctly ordered teardown.

## Publish

`pubnub_publish()` sends a message to all channel subscribers. The message is replicated across the network and delivered to every client currently subscribed to that channel.

### Method(s)

```c
pubnub_future_t pubnub_publish(pubnub_context_t* ctx, const pubnub_publish_opts_t* opts);
```

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channel | const | Yes | `—` | Borrowed, NUL-terminated. |
| message | const | Optional | `—` | Borrowed. Valid JSON string. Leave `message_len` at `0` for a NUL-terminated string, or set it for a length-counted one. Setting both `message` and `message_value` is `PUBNUB_ERR_INVALID_ARGUMENT`. |
| message_len | size_t | Optional | `0 (calls strlen)` | Byte count when `message` is not NUL-terminated. |
| message_value | pubnub_json_value_t* | Optional | `—` | Borrowed — the SDK serializes it during the call but does not consume it. Destroy it with `pubnub_json_destroy()` after `pubnub_publish()` returns. Setting both `message` and `message_value` is `PUBNUB_ERR_INVALID_ARGUMENT`. |
| method | pubnub_publish_method_t | Optional | `PUBNUB_PUBLISH_METHOD_GET` | `PUBNUB_PUBLISH_METHOD_GET` risks hitting the URL-length/message-size ceiling from percent-encoding. `PUBNUB_PUBLISH_METHOD_POST` sends the message in the request body and risks size growth only from encryption. |
| compress | pubnub_publish_compress_t | Optional | `PUBNUB_PUBLISH_COMPRESS_DEFAULT` | Request-body compression preference. Only takes effect when `method == PUBNUB_PUBLISH_METHOD_POST`; ignored for GET, which has no body to compress. `DEFAULT` follows the compile-time `PUBNUB_ENABLE_REQUEST_COMPRESSION` toggle, `YES` requests compression, `NO` opts this call out. When compression applies, the body is gzip-encoded with a `Content-Encoding: gzip` header. |
| store | pubnub_publish_store_t | Optional | `PUBNUB_PUBLISH_STORE_ACCOUNT_DEFAULT` | Whether to persist the message in Message Persistence. Set to `PUBNUB_PUBLISH_STORE_YES` to force storage regardless of the account default, or `PUBNUB_PUBLISH_STORE_NO` to force skipping it. |
| ttl | unsigned | Optional | `0 (account default)` | Hours to retain the message. Ignored when `store == PUBNUB_PUBLISH_STORE_NO`. |
| meta | const | Optional | `—` | Borrowed. Valid JSON string used for stream filtering. Setting both `meta` and `meta_value` is `PUBNUB_ERR_INVALID_ARGUMENT`. |
| meta_len | size_t | Optional | `0 (calls strlen)` | Byte count when `meta` is not NUL-terminated. |
| meta_value | pubnub_json_value_t* | Optional | `—` | Borrowed, same destroy-after-call contract as `message_value`. |
| custom_message_type | const | Optional | `—` | Borrowed, NUL-terminated. Must be 3-50 characters. |
| timeout_ms | uint32_t | Optional | `0 (inherits pubnub_config_t::transaction_timeout_ms)` | A non-zero value overrides the context-level timeout for this call only. |

Zero-initialize with `PUBNUB_PUBLISH_OPTS_INIT` (expands to `{0}`) so every unspecified field takes its protocol-safe default.

**C-family contract**

* **Header** — `#include <pubnub/features/publish.h>`
* **Types** — `pubnub_publish_opts_t`, `pubnub_publish_store_t`, `pubnub_publish_method_t`, `pubnub_publish_compress_t`
* **Prerequisite** — an initialized context with `publish_key`, `subscribe_key`, and `user_id` set
* **Feature flag** — `PUBNUB_ENABLE_PUBLISH`
* **Ownership / lifetime** — `message`/`meta` are borrowed, NUL-terminated or length-counted strings; `message_value`/`meta_value` are borrowed JSON trees the caller must destroy after the call returns
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

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

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

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

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

    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "my_channel";
    opts.message = "{\"text\":\"hello\"}";

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

    if (PUBNUB_OK == result) {
        pubnub_timetoken_t tt = pubnub_publish_result_timetoken(future);
        printf("published at %.*s\n", (int)tt.len, tt.ptr);
    }
    else {
        printf("publish failed: %d\n", (int)result);
    }

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

This sample is adapted from `examples/publish/sync.c` (blocking `pubnub_await`). The SDK also ships `examples/publish/async.c` (callback) and `examples/publish/cooperative.c` (cooperative polling). These are the same call, consumed the three ways described in [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md).

### Returns

```c
pubnub_timetoken_t pubnub_publish_result_timetoken(pubnub_future_t future);
```

Call this only after the future completes with `PUBNUB_OK`. It returns the timetoken the server assigned to the published message.

### Other examples

#### Publish a JSON value tree

Instead of building a JSON string by hand, construct a `pubnub_json_value_t*` tree with `PUBNUB_JSON_OBJ`/`PUBNUB_JSON_KV_*` (gated by `PUBNUB_CFG_JSON_HELPERS`) and pass it as `message_value`. Adapted from `examples/publish/value.c`.

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

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

    pubnub_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, "text", "hello"),
        PUBNUB_JSON_KV_INT(serial, "count", 3));

    pubnub_res_t result = PUBNUB_ERR_INTERNAL;
    if (NULL != tree) {
        pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
        opts.channel       = "my_channel";
        opts.message_value = tree;

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

        /* message_value is borrowed: destroy it after the call completes */
        pubnub_json_destroy(serial, tree);
    }

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

:::note Macro argument evaluation
Every `PUBNUB_JSON_KV_*` macro evaluates its `serial` argument (and, for `_STR`, its string argument) more than once. Pass only side-effect-free expressions.
:::

#### Publish several messages concurrently

Multiple in-flight futures can be tracked in an array and awaited independently. Adapted from `examples/publish/concurrent.c`.

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

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

#define MESSAGE_COUNT 3

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

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

    pubnub_future_t futures[MESSAGE_COUNT];
    const char*     bodies[MESSAGE_COUNT] = { "\"one\"", "\"two\"", "\"three\"" };

    for (int i = 0; i < MESSAGE_COUNT; i++) {
        pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
        opts.channel  = "my_channel";
        opts.message  = bodies[i];
        futures[i]    = pubnub_publish(ctx, &opts);
    }

    for (int i = 0; i < MESSAGE_COUNT; i++) {
        pubnub_res_t result = pubnub_await(futures[i]);
        printf("message %d: %s\n", i, (PUBNUB_OK == result) ? "ok" : "failed");
        pubnub_future_release(futures[i]);
    }

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

### Error responses

`pubnub_res_t` is the universal status type for this SDK; see [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full catalog. Two behaviors specific to publish are worth calling out:

* Setting both `message` and `message_value` (or both `meta` and `meta_value`) fails immediately with `PUBNUB_ERR_INVALID_ARGUMENT`, readable via `pubnub_future_status()` without polling or awaiting.
* Request capacity is two-tiered: while the in-flight pool (`PUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS`) has room, a publish starts immediately. Once it is exhausted, a new publish is placed in a pending queue (`PUBNUB_CFG_MAX_PENDING_REQUESTS`) and its future reports `PUBNUB_IN_PROGRESS`, not an error, until a slot frees up. `PUBNUB_ERR_QUEUE_FULL` is reserved for the case where both the in-flight pool and the pending queue are full.

## Signal

`pubnub_signal()` sends a signal, a small, unpersisted message, to all subscribers of a channel. Signals are always sent over HTTP GET and are never stored in Message Persistence.

| Feature | Signal | Message |
| --- | --- | --- |
| Payload size | 64 bytes (server-enforced) | Up to 32 KiB |
| Persistence | Never stored | Can be stored |
| Metadata (`meta`) | Not supported | Supported |
| HTTP method | Always GET | GET or POST |

### Method(s)

```c
pubnub_future_t pubnub_signal(pubnub_context_t* ctx, const pubnub_signal_opts_t* opts);
```

| Parameter | Description |
| --- | --- |
| `channel` *Type: `const char*`Default: — | Borrowed, NUL-terminated. |
| `message`Type: `const char*`Default: — | Borrowed. Valid JSON string. Setting both `message` and `message_value` is `PUBNUB_ERR_INVALID_ARGUMENT`. |
| `message_len`Type: `size_t`Default: `0` (calls `strlen`) | Byte count when `message` is not NUL-terminated. |
| `message_value`Type: `pubnub_json_value_t*`Default: — | Borrowed; destroy with `pubnub_json_destroy()` after the call returns. |
| `custom_message_type`Type: `const char*`Default: — | Borrowed, NUL-terminated, 3-50 characters. |
| `timeout_ms`Type: `uint32_t`Default: `0` (inherits context default) | Non-zero overrides the context-level timeout. |

There is no `meta`, `store`, `ttl`, or `method` field. Signals carry none of publish's persistence options. Zero-initialize with `PUBNUB_SIGNAL_OPTS_INIT`.

**C-family contract**

* **Header** — `#include <pubnub/features/signal.h>`
* **Types** — `pubnub_signal_opts_t`
* **Prerequisite** — an initialized context with `publish_key`, `subscribe_key`, and `user_id` set
* **Feature flag** — `PUBNUB_ENABLE_SIGNAL`
* **Ownership / lifetime** — same borrow rules as publish's `message`/`message_value`
* **Blocking** — never blocks; returns a `pubnub_future_t` immediately

### Sample code

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

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

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

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

    pubnub_signal_opts_t opts = PUBNUB_SIGNAL_OPTS_INIT;
    opts.channel = "my_channel";
    opts.message = "{\"lat\":1,\"lng\":2}";

    pubnub_future_t future = pubnub_signal(ctx, &opts);
    pubnub_res_t     result = pubnub_await(future);
    printf("signal: %s\n", (PUBNUB_OK == result) ? "ok" : "failed");

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

Adapted from `examples/signal/cooperative.c`, consumed here with blocking `pubnub_await` for brevity. `examples/signal/async.c` shows the callback style.

### Returns

```c
pubnub_timetoken_t pubnub_signal_result_timetoken(pubnub_future_t future);
```

Call this only after the future completes with `PUBNUB_OK`.

## Entities

Entities are thin, opaque handles that identify what to subscribe to. They carry no operations of their own. Every publish, signal, and subscribe function takes a context plus plain string identifiers, never an entity handle.

```c
pubnub_entity_t pubnub_channel(pubnub_context_t* ctx, const char* name);
pubnub_entity_t pubnub_channel_group(pubnub_context_t* ctx, const char* name);
pubnub_entity_t pubnub_channel_metadata(pubnub_context_t* ctx, const char* id);
pubnub_entity_t pubnub_user_metadata(pubnub_context_t* ctx, const char* id);

void                            pubnub_entity_destroy(pubnub_entity_t entity);
const char*                     pubnub_entity_name(pubnub_entity_t entity);
pubnub_subscribe_entity_type_t  pubnub_entity_type(pubnub_entity_t entity);
```

`pubnub_entity_type()` returns a `pubnub_subscribe_entity_type_t` identifying which factory produced the entity:

| Value | Meaning | Produced by |
| --- | --- | --- |
| `PUBNUB_SUBSCRIBE_CHANNEL` (`0`) | Regular channel. | `pubnub_channel()` |
| `PUBNUB_SUBSCRIBE_CHANNEL_GROUP` (`1`) | Channel group, resolved server-side. | `pubnub_channel_group()` |
| `PUBNUB_SUBSCRIBE_CHANNEL_METADATA` (`2`) | Channel metadata entity (App Context). | `pubnub_channel_metadata()` |
| `PUBNUB_SUBSCRIBE_USER_METADATA` (`3`) | User metadata entity (App Context). | `pubnub_user_metadata()` |

Do not confuse this enum with `pubnub_subscribe_message_type_t` (see [The subscribe event struct](#the-subscribe-event-struct)). Both share the `PUBNUB_SUBSCRIBE_` prefix, but one identifies what kind of entity you subscribed to and the other identifies what kind of event arrived. They are different types with different value sets.

Unlike most `const char*` fields in this SDK, the `name`/`id` argument to each factory function is **copied by the SDK**, not borrowed. The caller does not need to keep it alive past the call. Channel names have a server limit of 2048 characters. Channel-group names are limited to 92 characters.

An entity handle may be destroyed immediately after creating a subscription from it. `pubnub_subscription_create()`'s own doc states this explicitly, and both `examples/subscribe/callback.c` and `examples/arena_echo/arena_echo.c` do exactly that. There is no need to keep the entity alive for the lifetime of the subscription.

## Create a subscription

A subscription activates one entity so it starts receiving events. Creating a subscription does not itself start receiving events. Call `pubnub_subscription_subscribe()` to activate it.

### Method(s)

```c
pubnub_subscription_t pubnub_subscription_create(pubnub_entity_t entity, const pubnub_subscription_opts_t* opts);
void                   pubnub_subscription_destroy(pubnub_subscription_t sub);
pubnub_res_t           pubnub_subscription_subscribe(pubnub_subscription_t sub);
pubnub_res_t           pubnub_subscription_unsubscribe(pubnub_subscription_t sub);
```

| Parameter | Description |
| --- | --- |
| `with_presence`Type: `uint8_t`Default: `0` | `1` also subscribes to the entity's presence channel. Silently ignored for metadata entities. |

Passing `NULL` for `opts` to `pubnub_subscription_create()` is valid and uses the defaults. Zero-initialize an explicit struct with `PUBNUB_SUBSCRIPTION_OPTS_INIT`.

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>`
* **Types** — `pubnub_subscription_t`, `pubnub_subscription_opts_t`, `pubnub_entity_t`
* **Prerequisite** — an entity handle created from the same context
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`
* **Ownership / lifetime** — `pubnub_subscription_create()` consumes nothing from the entity; both handles can coexist, but the subscription itself must eventually be released with `pubnub_subscription_destroy()`
* **Blocking** — `pubnub_subscription_subscribe()`/`_unsubscribe()` return `pubnub_res_t` directly (no future); they do not block on network I/O to return, they only enqueue the state change

### Sample code

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

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

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

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

    pubnub_entity_t entity = pubnub_channel(ctx, "my_channel");

    pubnub_subscription_opts_t opts = PUBNUB_SUBSCRIPTION_OPTS_INIT;
    pubnub_subscription_t      sub  = pubnub_subscription_create(entity, &opts);

    /* the entity handle is no longer needed once the subscription exists */
    pubnub_entity_destroy(entity);

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

    /* ... register listeners and drive the event loop here ... */

    pubnub_subscription_unsubscribe(sub);
    pubnub_subscription_destroy(sub);
    pubnub_destroy(ctx);
    return EXIT_SUCCESS;
}
```

Adapted from `examples/subscribe/callback.c`.

### Returns

`pubnub_subscription_create()` returns a `pubnub_subscription_t` handle, or an invalid handle on failure. `pubnub_subscription_subscribe()`/`_unsubscribe()` return `PUBNUB_OK` on success, or `PUBNUB_ERR_INVALID_ARGUMENT` if `sub` is `NULL`.

## Subscription sets

A subscription set groups multiple subscriptions so they can be subscribed to, unsubscribed from, and listened to together.

### Method(s)

```c
pubnub_subscription_set_t pubnub_subscription_set_create(pubnub_context_t* ctx);
void                       pubnub_subscription_set_destroy(pubnub_subscription_set_t set);

pubnub_res_t pubnub_subscription_set_add_subscription(pubnub_subscription_set_t set, pubnub_subscription_t sub);
pubnub_res_t pubnub_subscription_set_remove_subscription(pubnub_subscription_set_t set, pubnub_subscription_t sub);
pubnub_res_t pubnub_subscription_set_add_subscription_set(pubnub_subscription_set_t target, pubnub_subscription_set_t other);
pubnub_res_t pubnub_subscription_set_remove_subscription_set(pubnub_subscription_set_t target, pubnub_subscription_set_t other);

pubnub_res_t pubnub_subscription_set_subscribe(pubnub_subscription_set_t set);
pubnub_res_t pubnub_subscription_set_unsubscribe(pubnub_subscription_set_t set);
```

A subscription set has no options struct. `pubnub_subscription_set_create()` takes only a context. Membership is managed entirely through the add/remove/merge functions above.

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>`
* **Types** — `pubnub_subscription_set_t`
* **Prerequisite** — one or more `pubnub_subscription_t` handles created on the same context
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`
* **Ownership / lifetime** — a subscription can belong to a set **and** be subscribed to individually at the same time; each acquisition (entity, subscription, set membership) holds an independent reference, and the underlying channel entry is released only when every reference is gone
* **Blocking** — none of these functions block; each returns `pubnub_res_t` directly

The same subscription can be a member of a set and subscribed individually at the same time. The shared registry entry tracks a reference count across all acquisitions, and a presence "leave" is sent only when the last reference is released. If a set is already subscribed when a new subscription is added to it, that new entry is auto-activated immediately, without a separate `subscribe()` call. The same is true when merging with `pubnub_subscription_set_add_subscription_set()`.

Destroying a set does not require destroying its members first: `pubnub_subscription_set_destroy()` releases the set's reference to every member in one call, and each member's underlying entry is freed only once nothing else references it.

:::warning Asymmetric not-found behavior
`pubnub_subscription_set_remove_subscription()` returns `PUBNUB_ERR_INVALID_ARGUMENT` if the subscription is not a member of the set. This is a hard error. `pubnub_subscription_set_remove_subscription_set()` silently skips any entries from `other` that are not found in `target`. This is not an error. Do not assume the two functions behave the same way on a missing entry.
:::

`pubnub_subscription_set_add_subscription_set()` also warns that a `PUBNUB_ERR_QUEUE_FULL` mid-merge may leave a **partial merge** in place. Check the return value rather than assuming the operation is atomic.

### Sample code

`pubnub_subscription_set_create()` returns `PUBNUB_SUBSCRIPTION_SET_INVALID` (a `NULL`-valued `pubnub_subscription_set_t`) if set creation fails. Check the returned handle against this sentinel before using it. Passing an invalid set to any of the other functions on this page returns `PUBNUB_ERR_INVALID_ARGUMENT`.

```c
pubnub_entity_t channel_a = pubnub_channel(ctx, "channel_a");
pubnub_entity_t channel_b = pubnub_channel(ctx, "channel_b");

pubnub_subscription_t sub_a = pubnub_subscription_create(channel_a, NULL);
pubnub_subscription_t sub_b = pubnub_subscription_create(channel_b, NULL);
pubnub_entity_destroy(channel_a);
pubnub_entity_destroy(channel_b);

pubnub_subscription_set_t set = pubnub_subscription_set_create(ctx);
if (PUBNUB_SUBSCRIPTION_SET_INVALID == set) {
    printf("subscription set creation failed\n");
    /* clean up sub_a/sub_b and return */
}

pubnub_subscription_set_add_subscription(set, sub_a);
pubnub_subscription_set_add_subscription(set, sub_b);
pubnub_subscription_set_subscribe(set);
```

### Returns

`pubnub_subscription_set_create()` returns a `pubnub_subscription_set_t` handle, or `PUBNUB_SUBSCRIPTION_SET_INVALID` on failure. Every other function above returns `pubnub_res_t`:

* `PUBNUB_OK` on success (including "already a member" for adds, which is de-duplicated rather than rejected)
* `PUBNUB_ERR_INVALID_ARGUMENT` for a `NULL`/invalid handle, a cross-context handle, or (for single-subscription removal only) a not-found member
* `PUBNUB_ERR_QUEUE_FULL` if the set is at capacity
* `PUBNUB_ERR_NOT_INITIALIZED` from `_subscribe()`/`_unsubscribe()` if subscribe support is not compiled in

## Listeners

A listener is a struct of optional callback fields. Register it at one of three levels, and it receives the events that level scopes to.

```c
typedef struct pubnub_subscribe_listener {
    pubnub_subscribe_status_cb_t         on_status;         /* const pubnub_subscribe_status_event_t*, void* */
    pubnub_subscribe_message_cb_t        on_message;        /* const pubnub_subscribe_event_t*, void* */
    pubnub_subscribe_signal_cb_t         on_signal;         /* const pubnub_subscribe_event_t*, void* */
    pubnub_subscribe_presence_cb_t       on_presence;       /* const pubnub_subscribe_event_t*, void* */
    pubnub_subscribe_message_action_cb_t on_message_action; /* const pubnub_subscribe_event_t*, void* */
    pubnub_subscribe_app_context_cb_t    on_app_context;    /* const pubnub_subscribe_event_t*, void* */
    pubnub_subscribe_file_cb_t           on_file;           /* const pubnub_subscribe_event_t*, void* */
    void* user_data;
} pubnub_subscribe_listener_t;
```

Every callback field is optional and individually nullable. Set only the ones you need. None of them receives the context as a parameter: every callback takes only a `const`-pointer event struct and the `user_data` you supplied at registration. If a callback needs the context, for example to call one of the [typed event extractors](#typed-event-extractors), carry it yourself through `user_data`.

`on_status` receives a `pubnub_subscribe_status_event_t*`. The other six callbacks all receive the same `pubnub_subscribe_event_t*` (see [The subscribe event struct](#the-subscribe-event-struct)). Both event structs are valid only for the duration of the callback invocation. Copy out any fields you need afterward.

### Method(s)

```c
pubnub_listener_handle_t pubnub_add_listener(pubnub_context_t* ctx, const pubnub_subscribe_listener_t* listener);
void                     pubnub_remove_listener(pubnub_context_t* ctx, pubnub_listener_handle_t handle);

pubnub_listener_handle_t pubnub_subscription_add_listener(pubnub_subscription_t sub, const pubnub_subscribe_listener_t* listener);
void                     pubnub_subscription_remove_listener(pubnub_subscription_t sub, pubnub_listener_handle_t handle);

pubnub_listener_handle_t pubnub_subscription_set_add_listener(pubnub_subscription_set_t set, const pubnub_subscribe_listener_t* listener);
void                     pubnub_subscription_set_remove_listener(pubnub_subscription_set_t set, pubnub_listener_handle_t handle);
```

| Registration level | Scope of data events (message/signal/presence/message action/app context/file) |
| --- | --- |
| `pubnub_add_listener(ctx, ...)` | Context-global — every active subscription on the context |
| `pubnub_subscription_add_listener(sub, ...)` | Only the entity that `sub` was created from |
| `pubnub_subscription_set_add_listener(set, ...)` | Only entities that are members of `set` |

Each `pubnub_add_listener()`/`pubnub_subscription_add_listener()`/`pubnub_subscription_set_add_listener()` call returns `PUBNUB_LISTENER_HANDLE_INVALID` if the combined listener pool (`PUBNUB_CFG_MAX_SUBSCRIBE_LISTENERS`, shared across all three registration levels) is already at capacity. Register listeners before or after calling `subscribe()`. No ordering constraint applies.

:::note on_status delivery
Register `on_status` through `pubnub_add_listener()`. Only context-global listeners receive status events. A per-subscription or per-set listener's `on_status` field is ignored. See [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for the full status/connection-state model.
:::

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>`
* **Types** — `pubnub_subscribe_listener_t`, `pubnub_listener_handle_t`
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`
* **Ownership / lifetime** — the listener struct itself is read at registration time; the SDK does not require it to remain live afterward, but any `user_data` it points to must stay valid for as long as the listener is registered
* **Thread / callback context** — callbacks run while the event loop is being driven ([Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md) covers cooperative, blocking, and callback consumption styles); keep callbacks fast and non-reentrant
* **Blocking** — registration/removal never block

### Sample code

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

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

/* on_status receives no context parameter, so carry it through user_data. */
typedef struct {
    pubnub_context_t* ctx;
} app_state_t;

static void on_status(const pubnub_subscribe_status_event_t* event, void* user_data)
{
    app_state_t* state = (app_state_t*)user_data;
    (void)state;
    printf("status: %d\n", (int)event->status);
}

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

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

    app_state_t state = { ctx };

    pubnub_subscribe_listener_t listener = { 0 };
    listener.on_status  = on_status;
    listener.user_data  = &state;

    pubnub_listener_handle_t handle = pubnub_add_listener(ctx, &listener);
    if (PUBNUB_LISTENER_HANDLE_INVALID == handle) {
        printf("listener registration failed\n");
    }

    /* ... subscribe and drive the event loop here ... */

    pubnub_remove_listener(ctx, handle);
    pubnub_destroy(ctx);
    return EXIT_SUCCESS;
}
```

Adapted from `examples/subscribe/callback.c`, with the `user_data`-carries-`ctx` idiom made explicit since `on_status` itself receives no context parameter.

### Returns

`pubnub_add_listener()`/`pubnub_subscription_add_listener()`/`pubnub_subscription_set_add_listener()` return a `pubnub_listener_handle_t`, or `PUBNUB_LISTENER_HANDLE_INVALID` if the listener pool is full. The corresponding `_remove_listener()` functions have no return value.

## The subscribe event struct

Six of the seven listener callbacks, every one except `on_status`, receive a `const pubnub_subscribe_event_t*`. This is the raw dispatched event. Pass it to the matching [typed event extractor](#typed-event-extractors) to get a struct with fields specific to that event's type.

```c
typedef struct pubnub_subscribe_event {
    pubnub_subscribe_message_type_t type;
    pubnub_string_view_t            channel;
    pubnub_string_view_t            subscription;
    pubnub_string_view_t            publisher;
    const pubnub_json_value_t*      payload;
    pubnub_string_view_t            custom_message_type;
    const pubnub_json_value_t*      user_metadata;
    pubnub_timetoken_t              timetoken;
    uint32_t                        flags;
} pubnub_subscribe_event_t;
```

| Field | Type | Description |
| --- | --- | --- |
| `type` | `pubnub_subscribe_message_type_t` | Discriminates the event. Pass the event to the extractor listed for this value in the table below. |
| `channel` | `pubnub_string_view_t` | Channel the event arrived on, with any `-pnpres` presence suffix removed. |
| `subscription` | `pubnub_string_view_t` | The wildcard or channel-group pattern that matched, when different from `channel`. |
| `publisher` | `pubnub_string_view_t` | Publisher user ID. May be empty for system-generated events. |
| `payload` | `const pubnub_json_value_t*` | Parsed message body (wire field `"d"`). For messages and signals, this is the published content. For presence, app context, and file events, it is the server-structured event body. `NULL` when absent. |
| `custom_message_type` | `pubnub_string_view_t` | Caller-supplied type tag (wire field `"cmt"`). May be empty. |
| `user_metadata` | `const pubnub_json_value_t*` | Parsed `meta` (wire field `"u"`). `NULL` when absent. |
| `timetoken` | `pubnub_timetoken_t` | Publish timetoken for this event, as a 17-digit decimal string view. |
| `flags` | `uint32_t` | Wire flags (wire field `"f"`). |

Every string-view field and both JSON-node fields (`payload`, `user_metadata`) are valid only for the duration of the listener callback that received this event. Copy out any bytes you need before returning.

### Discriminant values

`type` is a `pubnub_subscribe_message_type_t`:

| Value | Meaning | Matching extractor |
| --- | --- | --- |
| `PUBNUB_SUBSCRIBE_PRESENCE` (`-1`) | Presence event (join, leave, timeout, state change, interval). SDK-assigned from the `-pnpres` channel suffix, not a wire value. | `pubnub_subscribe_event_presence` |
| `PUBNUB_SUBSCRIBE_MESSAGE` (`0`) | Regular published message. Default when the wire `"e"` field is absent. | `pubnub_subscribe_event_message` |
| `PUBNUB_SUBSCRIBE_SIGNAL` (`1`) | Signal. | `pubnub_subscribe_event_signal` |
| `PUBNUB_SUBSCRIBE_APP_CONTEXT` (`2`) | App Context event (UUID, channel, or membership metadata change). | `pubnub_subscribe_event_app_context` |
| `PUBNUB_SUBSCRIBE_MESSAGE_ACTION` (`3`) | Message action added or removed. | `pubnub_subscribe_event_message_action` |
| `PUBNUB_SUBSCRIBE_FILE` (`4`) | File-sharing event. | `pubnub_subscribe_event_file` |

## Receiving messages — reading a JSON payload

Every message-like field in every subscribe event struct (`payload`, `message`, `state`, `joined`/`left`/`timed_out`, `data`) is `const struct pubnub_json_value*`. This is an opaque JSON-tree node, not a string. Printing it directly as a string will not compile. Use this pattern to read one:

1. Get the typed event struct from one of the [typed event extractors](#typed-event-extractors).
2. Check the JSON-node field for `NULL` before touching it.
3. Get the context's serialization provider with `pubnub_serialization(ctx)`.
4. **Check the specific vtable accessor for NULL.** Every accessor (`value_as_string`, `object_get`, `array_get`, `array_size`, and so on) is individually optional.
5. Read the value: `value_as_string()` for a string leaf, `object_get(node, key, keylen)` for an object field, `array_size()`/`array_get()` to walk an array.
6. Copy out any bytes you need. Every pointer this pattern returns is valid **only inside the current listener callback**.

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

#include <stdio.h>

static void print_message_text(pubnub_context_t* ctx, const pubnub_subscribe_message_event_t* msg)
{
    if (NULL == msg->message) {
        return;
    }

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

    size_t      len = 0;
    const char* val = serial->value_as_string(msg->message, &len);
    if (NULL != val) {
        /* val is NOT NUL-terminated, and valid only inside this callback */
        printf("message: %.*s\n", (int)len, val);
    }
}
```

Two extended cases, both from the same pattern:

```c
/* Object field: read a named key out of a JSON object node. */
static void print_mood(pubnub_serialization_provider_t* serial, const pubnub_json_value_t* state)
{
    if (NULL == serial || NULL == serial->object_get || NULL == serial->value_as_string) {
        return;
    }
    const pubnub_json_value_t* mood = serial->object_get(state, "mood", 4);
    if (NULL == mood) {
        return;
    }
    size_t      len = 0;
    const char* val = serial->value_as_string(mood, &len);
    if (NULL != val) {
        printf("mood: %.*s\n", (int)len, val);
    }
}

/* Array field: walk a JSON array node element by element. */
static void print_joined(pubnub_serialization_provider_t* serial, const pubnub_json_value_t* joined)
{
    if (NULL == serial || NULL == serial->array_size || NULL == serial->array_get
        || NULL == serial->value_as_string) {
        return;
    }
    size_t count = serial->array_size(joined);
    for (size_t i = 0; i < count; i++) {
        const pubnub_json_value_t* elem = serial->array_get(joined, i);
        size_t                     len  = 0;
        const char*                val  = serial->value_as_string(elem, &len);
        if (NULL != val) {
            printf("%.*s ", (int)len, val);
        }
    }
}
```

:::note No shortcut to a raw string
There is no accessor that returns a `pubnub_string_view_t` over an arbitrary node's raw bytes. `value_as_string()` only works on nodes that are already string leaves. To re-serialize an arbitrary node back to wire bytes, use `serial->serialize(serial, node, buffer, buffer_capacity, &out_len)` with a caller-provided output buffer, and check both the returned `pubnub_res_t` and `out_len`.
:::

## Typed event extractors

Each extractor validates the event's type and, on success, fills a typed output struct whose fields you read with the pattern above. All six share the same contract:

* `PUBNUB_OK` on success
* `PUBNUB_ERR_INVALID_ARGUMENT` for a `NULL` argument or a type mismatch
* `PUBNUB_ERR_SERIALIZATION` on parse failure
* the output struct is zero-initialized on any failure

| Extractor | Output struct |
| --- | --- |
| `pubnub_subscribe_event_message(ctx, event, out)` | `pubnub_subscribe_message_event_t` |
| `pubnub_subscribe_event_signal(ctx, event, out)` | `pubnub_subscribe_signal_event_t` |
| `pubnub_subscribe_event_presence(ctx, event, out)` | `pubnub_subscribe_presence_event_t` |
| `pubnub_subscribe_event_message_action(ctx, event, out)` | `pubnub_subscribe_message_action_event_t` |
| `pubnub_subscribe_event_app_context(ctx, event, out)` | `pubnub_subscribe_app_context_event_t` |
| `pubnub_subscribe_event_file(ctx, event, out)` | `pubnub_subscribe_file_event_t` |

Every pointer/view field in every output struct below is valid only for the duration of the listener callback that produced it. Copy out any bytes you need to keep.

#### Message

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

    pubnub_subscribe_message_event_t msg;
    if (PUBNUB_OK != pubnub_subscribe_event_message(ctx, event, &msg)) {
        return;
    }
    if (NULL == msg.message) {
        return;
    }

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

`pubnub_subscribe_message_event_t` has `channel`, `subscription`, `publisher`, `timetoken`, `custom_message_type` (string views), plus `message` and `user_metadata` (JSON-node pointers).

#### Signal

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

    pubnub_subscribe_signal_event_t sig;
    if (PUBNUB_OK != pubnub_subscribe_event_signal(ctx, event, &sig)) {
        return;
    }
    if (NULL == sig.message) {
        return;
    }

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

`pubnub_subscribe_signal_event_t` has the same shape as the message event, with its JSON-node field also named `message`.

#### Presence

For the full set of Presence operations (`here_now`, `where_now`, user state), see [Presence](https://www.pubnub.com/docs/sdks/c/api-reference/presence.md). This extractor is documented here only because it is one of the six typed subscribe events.

```c
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;
    }

    pubnub_serialization_provider_t* serial = pubnub_serialization(ctx);
    if (NULL == pres.joined || NULL == serial
        || NULL == serial->array_size || NULL == serial->array_get || NULL == serial->value_as_string) {
        return;
    }
    size_t count = serial->array_size(pres.joined);
    for (size_t i = 0; i < count; i++) {
        const pubnub_json_value_t* elem = serial->array_get(pres.joined, i);
        size_t                     len  = 0;
        const char*                val  = serial->value_as_string(elem, &len);
        if (NULL != val) {
            printf("%.*s ", (int)len, val);
        }
    }
}
```

`pubnub_subscribe_presence_event_t` has `action`, `uuid`, `channel`, `subscription`, `occupancy`, `timetoken`, `here_now_refresh`, plus `state`, `joined`, `left`, and `timed_out` as JSON-node pointers. Apply the same pattern to `left`/`timed_out`/`state` as shown above for `joined`.

#### Message action

`pubnub_subscribe_message_action_event_t` is the one extractor whose fields are all `pubnub_string_view_t`. There is no JSON node to read here.

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

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

#### App context

`object_type` is an enum, not a string view. See [Presence](https://www.pubnub.com/docs/sdks/c/api-reference/presence.md) and [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md) for related context on entities. The full set of App Context operations is out of scope for this page.

```c
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);
}
```

#### File

`pubnub_subscribe_file_event_t` has `channel`, `subscription`, `publisher`, `file_id`, `file_name` (string views), `message` (a JSON-node pointer), and `timetoken`.

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

    pubnub_subscribe_file_event_t file;
    if (PUBNUB_OK == pubnub_subscribe_event_file(ctx, event, &file)) {
        printf("file_id: %.*s, file_name: %.*s\n",
               (int)file.file_id.len, file.file_id.ptr,
               (int)file.file_name.len, file.file_name.ptr);
    }
}
```

## Subscribe controls

These functions act on the whole context rather than on an individual subscription or set.

```c
pubnub_res_t                          pubnub_subscribe_disconnect(pubnub_context_t* ctx);
pubnub_res_t                          pubnub_subscribe_reconnect(pubnub_context_t* ctx);
pubnub_res_t                          pubnub_subscribe_restore(pubnub_context_t* ctx, pubnub_timetoken_t timetoken);
pubnub_res_t                          pubnub_subscribe_unsubscribe_all(pubnub_context_t* ctx);
pubnub_subscribe_connection_state_t   pubnub_subscribe_state(const pubnub_context_t* ctx);
```

There is no `pubnub_subscribe_async()` function. On threaded builds (`PUBNUB_CFG_THREAD_SAFETY=1`), a background thread starts automatically to drive I/O and deliver listener callbacks once the first subscription is activated, no separate call is needed. On cooperative builds, call `pubnub_process(ctx)` in your event loop instead.

No file under `examples/` calls any of these functions. The signatures and behavior come directly from `features/subscribe.h`.

`pubnub_subscribe_disconnect()` stops the subscribe loop for all channels and groups without unregistering them. If the client was actively receiving, listeners get a `PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED` status event. If it was still handshaking, the handshake is cancelled and no status event fires. Subscriptions stay registered and can be restarted with `pubnub_subscribe_reconnect()`.

`pubnub_subscribe_restore(ctx, timetoken)` saves `timetoken` as a resume cursor and emits a `SUBSCRIPTION_RESTORED` status event. If the context is currently connected, the next receive cycle starts from that cursor without a full re-handshake. If it is stopped or disconnected, the cursor is only saved. Call `pubnub_subscribe_reconnect()` afterward to actually resume from it. This is the pattern for resuming a subscription across an app restart:

```c
pubnub_subscribe_restore(ctx, saved_timetoken);
pubnub_subscribe_reconnect(ctx);
```

Passing a zero-length or `NULL` `timetoken` emits `SUBSCRIPTION_RESTORED` using whatever cursor is already stored internally. This is equivalent to `pubnub_subscribe_reconnect()` for resume position. The `pubnub_timetoken_t` view passed in is consumed by the call and does not need to outlive it.

**C-family contract**

* **Header** — `#include <pubnub/features/subscribe.h>`
* **Feature flag** — `PUBNUB_ENABLE_SUBSCRIBE`
* **Blocking** — none of these block

### Returns

The result/status codes these functions can return, and the connection-state machine `pubnub_subscribe_state()` reports on, are covered in full in [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md). This page does not re-derive that transition table.

## Error handling

`pubnub_res_t` is the single status type shared across publish, signal, and subscribe. There are no per-feature error accessors. For the full result-code catalog, connection states, and the `pubnub_response_service_error()` pattern for server-side error detail, see [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md).

## 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.
* **Signal** - A non-persistent message limited to 64 bytes designed for high-volume usecases where the the most recent data is relevant, like GPS location updates.

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