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

# C API & SDK Docs 1.0.0

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.

This guide walks you through a simple "Hello, world" application that demonstrates the core concepts of the PubNub C SDK:

* Setting up a connection
* Sending a message
* Receiving a message in real time

## Overview

The C SDK targets embedded and native C environments: hosted POSIX systems (Linux, macOS), Windows, and constrained targets (FreeRTOS, ESP-IDF). This tutorial builds against the `full` build profile, a hosted configuration with every feature enabled. It needs nothing beyond a C toolchain and network access, making it the fastest path to a running program. The SDK has exactly one subscription model and one asynchronous model, so nothing here changes if you move to a different profile or platform later. Only the build configuration changes. For the other platforms, and the details this page skips, see [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md).

## Prerequisites

| Requirement | Notes |
| --- | --- |
| CMake | Version 3.16 or later |
| C compiler | C11-capable (GCC, Clang, or AppleClang). If your compiler cannot support C11, configure with `-DPUBNUB_CFG_C99_COMPAT=ON` |
| POSIX threads | Required on Linux and macOS |
| libcurl (≥ 7.66), OpenSSL, cJSON | Used automatically if already installed. Otherwise CMake's `FetchContent` fetches and builds libcurl and cJSON on first configure |
| A PubNub account and keyset | A `publish_key` and `subscribe_key` from the PubNub Admin Portal. See [Get your PubNub keys](#get-your-pubnub-keys) below |
| A `user_id` | Any non-empty string identifying this client. It's a required configuration field, and an empty or missing value is rejected |

Because CMake's `FetchContent` fetches cJSON, and libcurl if not already present on your system, your first `cmake` configure needs network access.

:::tip Building for other targets
This page covers only the `full` profile. For Windows, FreeRTOS, and ESP-IDF, see [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md#prerequisites), which documents all three build paths and their CI-verification status.
:::

## Setup

### Get your PubNub keys

First, get your PubNub keys:

* [Sign in](https://admin.pubnub.com/#/login) or [create an account](https://admin.pubnub.com/#/signup) on the PubNub Admin Portal.
* Create an app (or use an existing one).
* Find your publish and subscribe keys in the app dashboard.

When you create an app, PubNub automatically generates a keyset. You can use the same keyset for development and production, but separate keysets per environment improve security and management.

### Install the SDK

The SDK source is on GitHub at [pubnub/c](https://github.com/pubnub/c). There is no installable CMake package for this SDK. Vendor the source tree and add it as a CMake subdirectory in the same configure pass as your application:

```cmake
add_subdirectory(third_party/pubnub-c)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE pubnub)
```

`pubnub` is a library target, static by default or shared when built with `-DPUBNUB_BUILD_SHARED=ON`, that aggregates the core library, every enabled feature module, and every selected provider backend, so linking it is sufficient. For this tutorial, you can build and run the SDK's own bundled example instead of a project of your own. From a checkout of the SDK, configure and build the `full` profile:

```bash
cmake --preset full
cmake --build --preset full --target example_subscribe_callback
```

These commands, and the one in [Run the app](#run-the-app) below, come straight from the SDK's own build configuration. See [Consuming the SDK from your own project](https://www.pubnub.com/docs/sdks/c/environment-setup.md#consuming-the-sdk-from-your-own-project) for `FetchContent` and git-submodule alternatives to vendoring.

## Steps

### Initialize PubNub

Every program starts by populating a PubNub configuration and creating a context from it. Always call `pubnub_config_defaults()` rather than zero-initializing `pubnub_config_t` by hand: it fills in the request timeouts, enables TCP keepalive at 60-second idle / 20-second interval / 3 probes, sets an exponential retry policy for subscribe requests, and sets the log level to `PUBNUB_LOG_LEVEL_INFO`. It does not set `publish_key`, `subscribe_key`, or `user_id` for you. A hand-rolled `pubnub_config_t cfg = {0};` leaves both timeouts at `0`, keepalive disabled, and no automatic retry at all.

```c
static pubnub_context_t* initialize_pubnub(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.publish_key   = "demo";
    cfg.subscribe_key = "demo";
    cfg.user_id       = "my_unique_user_id";

    return pubnub_create(&cfg);
}
```

`pubnub_create()` allocates the context on the heap and deep-copies the strings in `cfg`. Always check its return value for `NULL` before using the context. See [Initialization](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#initialization) for the alternate `pubnub_init()`/`pubnub_deinit()` model, which uses caller-provided memory instead of the heap and is the only option under `PUBNUB_CFG_NO_HEAP=1`.

### Set up event listeners

A listener is a struct of optional callback fields. Register it once, before or after subscribing. Registration order does not matter. Every callback receives only a `const`-pointer event struct and the `user_data` you supplied at registration. **None of them receives the context as a parameter.** If a callback needs the context, carry it through `user_data`. That's why the state struct below stores it.

```c
typedef struct app_state {
    pubnub_context_t* ctx;
    int                connected;
    int                message_received;
} 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;

    if (PUBNUB_SUBSCRIBE_STATUS_CONNECTED == event->status) {
        state->connected = 1;
        printf("STATUS: connected\n");
    }
    else if (PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED == event->status
             || PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLY == event->status
             || PUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR == event->status) {
        state->connected = 0;
        printf("STATUS: disconnected (%s)\n", pubnub_res_str(event->reason));
    }
}

/* on_message is defined in "Receive messages" below. */
static void on_message(const pubnub_subscribe_event_t* event, void* user_data);

static pubnub_listener_handle_t register_listeners(pubnub_context_t* ctx, app_state_t* state)
{
    pubnub_subscribe_listener_t listener = {
        .on_status  = on_status,
        .on_message = on_message,
        .user_data  = state,
    };

    return pubnub_add_listener(ctx, &listener);
}
```

`pubnub_add_listener()` registers the listener at the context-global level, the level the SDK documents as receiving `on_status`. It returns `PUBNUB_LISTENER_HANDLE_INVALID` if the listener pool is already full. See [Listeners](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#listeners) for the other two registration levels (per-subscription, per-set) and the full callback field list. See [Connection states](https://www.pubnub.com/docs/sdks/c/status-events.md#connection-states) for every `pubnub_subscribe_status_t` value beyond the three handled above.

### Create a subscription

Subscribing is a two-step process: create an entity handle for what you want to subscribe to, then create and activate a subscription from it. The entity handle can be destroyed immediately after the subscription is created. It does not need to stay alive for the subscription's lifetime.

```c
static pubnub_subscription_t create_channel_subscription(pubnub_context_t* ctx)
{
    pubnub_entity_t entity = pubnub_channel(ctx, "my_channel");
    if (NULL == entity) {
        return NULL;
    }

    pubnub_subscription_t sub = pubnub_subscription_create(entity, NULL);
    pubnub_entity_destroy(entity);
    if (NULL == sub) {
        return NULL;
    }

    if (PUBNUB_OK != pubnub_subscription_subscribe(sub)) {
        pubnub_subscription_destroy(sub);
        return NULL;
    }

    return sub;
}
```

Passing `NULL` for the options argument to `pubnub_subscription_create()` uses its defaults. `pubnub_subscription_subscribe()` only enqueues the state change. It does not block on network I/O, and the actual handshake completes asynchronously, observed through the `on_status` callback set up in the previous step. See [Entities](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#entities) and [Create a subscription](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#create-a-subscription) for the options struct and the full function set. See [Subscription sets](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#subscription-sets) for grouping several subscriptions together, which this tutorial does not need.

### Publish messages

`pubnub_publish()` takes a context and a pointer to an options struct, not a channel and message string directly. It never blocks. It returns a `pubnub_future_t` immediately, which you then consume with whichever of the three consumption styles fits your program. This tutorial uses the simplest one, blocking `pubnub_await()`:

```c
static pubnub_res_t publish_hello(pubnub_context_t* ctx)
{
    pubnub_future_t future = pubnub_publish(ctx,
                                             &(pubnub_publish_opts_t){
                                                 .channel = "my_channel",
                                                 .message = "\"Hello, world!\"",
                                             });

    pubnub_res_t result = pubnub_await(future);
    if (PUBNUB_OK != result) {
        pubnub_string_view_t msg = pubnub_response_error_message(future);
        printf("publish failed: %s - %.*s\n",
               pubnub_res_str(result), (int)msg.len, msg.ptr ? msg.ptr : "");
    }
    pubnub_future_release(future);
    return result;
}
```

Release every future exactly once, regardless of which consumption style you used. Because this program is also subscribed to `my_channel`, the published message arrives back through the subscribe stream and is delivered to `on_message`. `pubnub_response_error_message()` returns the server's error body when it can be extracted from the response, and an empty `{NULL, 0}` view otherwise (for example, on a transport-level failure with no server reply). See [Publish](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#publish) for the full options struct, and [Calling patterns](https://www.pubnub.com/docs/sdks/c/environment-setup.md#calling-patterns) for cooperative polling and callback-driven publishing.

### Receive messages

A received message payload is a JSON-tree node pointer, not a string. `printf("%s", msg.message)` does not compile, and would not print anything sensible even with a stray cast. Read it with this pattern: get the typed event through `pubnub_subscribe_event_message()`, check the `message` field for `NULL`, get the context's serialization provider with `pubnub_serialization(ctx)`, check that specific vtable accessor for `NULL`, then call it.

```c
/* app_state_t is the struct defined in Set up event listeners, above.
 * It's not redefined here: this callback is only ever compiled
 * alongside that definition, as in the complete example below. */
static void on_message(const pubnub_subscribe_event_t* event, void* user_data)
{
    app_state_t* state = (app_state_t*)user_data;
    state->message_received = 1;

    pubnub_subscribe_message_event_t msg;
    if (PUBNUB_OK != pubnub_subscribe_event_message(state->ctx, event, &msg)) {
        printf("MSG [parse error]\n");
        return;
    }

    printf("MSG [%.*s]: ", (int)msg.channel.len, msg.channel.ptr);

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

(`app_state_t` is the struct defined in [Set up event listeners](#set-up-event-listeners). Do not paste both definitions into the same file.) Every vtable accessor on the serialization provider is individually optional and must be checked for `NULL` before use. Every pointer this pattern returns, including `val`, is valid only for the duration of this callback, so copy out any bytes you need afterward. See [Receiving messages — reading a JSON payload](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md#receiving-messages--reading-a-json-payload) for the object and array-node variants of this same pattern. See [Accessing the serialization provider](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md#accessing-the-serialization-provider) for the provider itself.

### Run the app

Run the binary you built in [Install the SDK](#install-the-sdk):

```bash
./build/full/examples/subscribe/example_subscribe_callback
```

You should see output similar to the following, matching the [complete example](#complete-example) below:

```text
STATUS: connected
MSG [my_channel]: "Hello, world!"
```

## Complete example

The sections above factor each step into its own function for clarity. The complete program below inlines that same logic into `main()` and adds the piece those functions leave out: driving the event loop with `pubnub_process()` while waiting for the connection handshake and the message to arrive. It also tears everything down in order on every exit path.

:::note POSIX-only sleep
This example uses `nanosleep()` from `<time.h>`, which is POSIX-only and unavailable on Windows. On Windows, replace it with `Sleep(10)` from `<windows.h>`, or use a platform-agnostic sleep utility.
:::

```c
#include <pubnub/pubnub.h>
#include <pubnub/providers/serialization.h>

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

typedef struct app_state {
    pubnub_context_t* ctx;
    int                connected;
    int                message_received;
} 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;

    if (PUBNUB_SUBSCRIBE_STATUS_CONNECTED == event->status) {
        state->connected = 1;
        printf("STATUS: connected\n");
    }
    else if (PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED == event->status
             || PUBNUB_SUBSCRIBE_STATUS_DISCONNECTED_UNEXPECTEDLY == event->status
             || PUBNUB_SUBSCRIBE_STATUS_CONNECTION_ERROR == event->status) {
        state->connected = 0;
        printf("STATUS: disconnected (%s)\n", pubnub_res_str(event->reason));
    }
}

static void on_message(const pubnub_subscribe_event_t* event, void* user_data)
{
    app_state_t* state = (app_state_t*)user_data;
    state->message_received = 1;

    pubnub_subscribe_message_event_t msg;
    if (PUBNUB_OK != pubnub_subscribe_event_message(state->ctx, event, &msg)) {
        printf("MSG [parse error]\n");
        return;
    }

    printf("MSG [%.*s]: ", (int)msg.channel.len, msg.channel.ptr);

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

int main(void)
{
    int status = 0;

    /* 1. Configure and create the client. */
    app_state_t state = { .ctx = NULL, .connected = 0, .message_received = 0 };

    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) {
        printf("pubnub_create failed\n");
        return EXIT_FAILURE;
    }
    state.ctx = ctx;

    /* 2. Register a listener before subscribing (order is not required,
     * but doing it first means no event can be missed). */
    pubnub_subscribe_listener_t listener = {
        .on_status  = on_status,
        .on_message = on_message,
        .user_data  = &state,
    };

    pubnub_listener_handle_t lh = pubnub_add_listener(ctx, &listener);
    if (PUBNUB_LISTENER_HANDLE_INVALID == lh) {
        printf("Failed to register listener\n");
        status = 1;
        goto cleanup_context;
    }

    /* 3. Create a channel entity and a subscription from it. The
     * entity may be destroyed immediately after the subscription
     * is created -- it is not needed afterward. */
    pubnub_entity_t entity = pubnub_channel(ctx, "my_channel");
    if (NULL == entity) {
        printf("Failed to create channel entity\n");
        status = 1;
        goto cleanup_listener;
    }

    pubnub_subscription_t sub = pubnub_subscription_create(entity, NULL);
    pubnub_entity_destroy(entity);
    if (NULL == sub) {
        printf("Failed to create subscription\n");
        status = 1;
        goto cleanup_listener;
    }

    /* 4. Activate the subscription, then wait for the connection
     * handshake (on_status sets state.connected = 1). */
    pubnub_res_t rc = pubnub_subscription_subscribe(sub);
    if (PUBNUB_OK != rc) {
        printf("subscribe failed: %s\n", pubnub_res_str(rc));
        status = 1;
        goto cleanup_subscription;
    }

    {
        const struct timespec nap             = { .tv_sec = 0, .tv_nsec = 10000000L };
        const time_t           handshake_start = time(NULL);
        while (!state.connected) {
            pubnub_process(ctx);
            if (difftime(time(NULL), handshake_start) > 10.0) {
                printf("Handshake timeout -- giving up.\n");
                status = 1;
                goto cleanup_subscription;
            }
            nanosleep(&nap, NULL);
        }
    }

    /* 5. Publish a message. It will arrive back through the
     * subscribe stream and be delivered to on_message above. */
    pubnub_future_t pub_future = pubnub_publish(ctx,
                                                 &(pubnub_publish_opts_t){
                                                     .channel = "my_channel",
                                                     .message = "\"Hello, world!\"",
                                                 });

    pubnub_res_t pub_rc = pubnub_await(pub_future);
    if (PUBNUB_OK != pub_rc) {
        pubnub_string_view_t pub_err = pubnub_response_error_message(pub_future);
        printf("publish failed: %s - %.*s\n",
               pubnub_res_str(pub_rc), (int)pub_err.len, pub_err.ptr ? pub_err.ptr : "");
    }
    pubnub_future_release(pub_future);

    /* 6. Wait for the echoed message to arrive. */
    {
        const struct timespec nap        = { .tv_sec = 0, .tv_nsec = 10000000L };
        const time_t           wait_start = time(NULL);
        while (!state.message_received) {
            pubnub_process(ctx);
            if (difftime(time(NULL), wait_start) > 5.0) {
                printf("Timed out waiting for the message.\n");
                break;
            }
            nanosleep(&nap, NULL);
        }
    }

    /* 7. Clean shutdown. pubnub_subscription_unsubscribe() is called
     * explicitly here so the SUBSCRIPTION_CHANGED event fires before
     * teardown, but it is not required: pubnub_subscription_destroy()
     * deactivates the subscription itself if it is still active, so
     * every goto path above that skips straight to cleanup_subscription
     * (destroy without a prior unsubscribe) is also safe. */
    pubnub_subscription_unsubscribe(sub);
cleanup_subscription:
    pubnub_subscription_destroy(sub);
cleanup_listener:
    pubnub_remove_listener(ctx, lh);
cleanup_context:
    pubnub_destroy(ctx);

    return status;
}
```

## Next steps

Build features:

* [Presence](https://www.pubnub.com/docs/sdks/c/api-reference/presence.md) — track who else is on a channel.
* [Message Persistence](https://www.pubnub.com/docs/sdks/c/api-reference/storage-and-playback.md) — store and retrieve past messages.
* [Access Manager](https://www.pubnub.com/docs/sdks/c/api-reference/access-manager.md) — secure your channels with grant tokens.
* [Channel Groups](https://www.pubnub.com/docs/sdks/c/api-reference/channel-groups.md) — organize many channels under one subscription.
* [Encryption](https://www.pubnub.com/docs/sdks/c/api-reference/encryption.md) — encrypt message payloads and files.

Go deeper:

* [Publish & Subscribe](https://www.pubnub.com/docs/sdks/c/api-reference/publish-and-subscribe.md) — the full options structs, subscription sets, signals, and every typed event extractor.
* [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md) — the full `pubnub_config_t` field reference, proxy configuration, TCP keepalive, and provider pointers.
* [Environment Setup](https://www.pubnub.com/docs/sdks/c/environment-setup.md) — build profiles and presets, the three calling patterns compared, thread safety, and memory tuning for constrained targets.

Operate:

* [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) — the full connection-state model, the `pubnub_res_t` result catalog, and retrieving server error detail.
* [Logging](https://www.pubnub.com/docs/sdks/c/logging.md) — configure the logger provider.

## Terms in this document

* **DataSync entity** - A stored server-side object in DataSync with system fields (id, eTag, timestamps) and a free-form JSON payload, typed by exactly one entity class. Not the same as an SDK entity, which is only a local handle.
* **Listener** - A function or objectthat reacts to events or messages, like new chat messages or connection updates, letting your app respond in real-time.
* **Message** - A unit of data transmitted between clients or between a client and a server in PubNub, containing information such as text, binary data, or structured data formats like JSON. Messages are sent over channels and can be tracked for delivery and read status.
* **Publish Key** - A unique identifier that allows your application to send messages to PubNub channels. It's part of your app's credentials and should be kept secure.
* **PubNub** - PubNub is a real-time messaging platform that provides APIs and SDKs for building scalable applications. It handles the complex infrastructure of real-time communication, including: Message delivery and persistence, Presence detection, Access control, Push notifications, File sharing, Serverless processing with Functions and Events & Actions, Analytics and monitoring with BizOps Workspace, AI-powered insights with Illuminate.
* **SDK entity** - A local, client-side handle within a PubNub SDK that allows you to perform context-specific operations on one channel, user, or metadata record. Creating one makes no network call and needs no matching server-side record. Not the same as a DataSync entity.
* **Subscribe Key** - A unique identifier that allows your application to receive messages from PubNub channels. It's part of your app's credentials and should be kept secure.

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