On this page

C API & SDK Docs 1.0.0

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.

Prerequisites

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

Building for other targets

This page covers only the full profile. For Windows, FreeRTOS, and ESP-IDF, see Environment Setup, which documents all three build paths and their CI-verification status.

Setup

Get your PubNub keys

First, get your PubNub keys:

  • Sign in or create an account 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. 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:

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:

cmake --preset full
cmake --build --preset full --target example_subscribe_callback

These commands, and the one in Run the app below, come straight from the SDK's own build configuration. See 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.

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

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
show all 35 lines

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 for the other two registration levels (per-subscription, per-set) and the full callback field list. See 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.

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);
show all 20 lines

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 and Create a subscription for the options struct and the full function set. See 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():

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);
show all 17 lines

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 for the full options struct, and 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.

/* 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);
show all 28 lines

(app_state_t is the struct defined in 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 for the object and array-node variants of this same pattern. See Accessing the serialization provider for the provider itself.

Run the app

Run the binary you built in Install the SDK:

./build/full/examples/subscribe/example_subscribe_callback

You should see output similar to the following, matching the complete example below:

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.

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.

#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)
{
show all 176 lines

Next steps

Build features:

Go deeper:

  • Publish & Subscribe — the full options structs, subscription sets, signals, and every typed event extractor.
  • Configuration — the full pubnub_config_t field reference, proxy configuration, TCP keepalive, and provider pointers.
  • Environment Setup — build profiles and presets, the three calling patterns compared, thread safety, and memory tuning for constrained targets.

Operate:

  • Status Events — the full connection-state model, the pubnub_res_t result catalog, and retrieving server error detail.
  • Logging — configure the logger provider.