---
source_url: https://www.pubnub.com/docs/getting-started/quickstart
title: Quickstart
updated_at: 2026-09-30T07:20:08.000Z
---

# Quickstart

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

In this quickstart, you publish a message to a PubNub channel and receive it in the same program. That's your first end-to-end real-time exchange with PubNub. Pick your language above. Every step and code sample on this page follows it.

## Before you begin

Make sure you have a PubNub account and your own keyset. A keyset is the set of publish, subscribe, and secret keys that identifies your application to the PubNub network.

If you don't have one yet, the fastest way to get one is using a Prototype:

1. [Sign up](https://admin.pubnub.com/#/register) or [log in](https://admin.pubnub.com/) to Admin Portal.
2. In the **Recent prototypes** section, click the app. A new app is created automatically when you log in for the first time.
3. Copy the publish key and subscribe keys.

:::tip Create a keyset manually
You can also follow [Set up your account](https://www.pubnub.com/docs/architecture/authentication/set-up-your-account.md) to create one, then come back here.
:::

## Set up the project

### JavaScript

Make sure you have Node.js 22 or later installed, then create a new directory and install the PubNub JavaScript SDK:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
npm init -y
npm install pubnub
```

Then create a file called `index.js` in that directory.

### Python

The PubNub Python SDK requires Python 3.9 or later. Create a new directory with a virtual environment and install the SDK:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
python3 -m venv venv
source venv/bin/activate
pip install pubnub
```

Then create a file called `main.py` in that directory.

### Java

Make sure you have JDK 17 or later and Maven installed, then create the project layout:

```bash
mkdir -p pubnub-quickstart/src/main/java
cd pubnub-quickstart
```

Create `pom.xml` with the PubNub Java SDK dependency:

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>pubnub-quickstart</artifactId>
  <version>1.0</version>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>com.pubnub</groupId>
      <artifactId>pubnub-gson</artifactId>
      <version>13.4.4</version>
    </dependency>
  </dependencies>
</project>
```

Then create a file called `src/main/java/App.java`.

### Kotlin

Make sure you have JDK 17 or later and Gradle installed, then create the project layout:

```bash
mkdir -p pubnub-quickstart/src/main/kotlin
cd pubnub-quickstart
```

Create `settings.gradle.kts`:

```kotlin
rootProject.name = "pubnub-quickstart"
```

Create `build.gradle.kts` with the PubNub Kotlin SDK dependency:

```kotlin
plugins {
    kotlin("jvm") version "2.2.20"
    application
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.pubnub:pubnub-kotlin:13.4.4")
}

kotlin {
    jvmToolchain(17)
}

application {
    mainClass = "MainKt"
}
```

Then create a file called `src/main/kotlin/Main.kt`.

### C#

Make sure you have the .NET SDK 9.0 or later installed, then create a console project and add the PubNub package. `PubnubPCL` is the package for .NET Standard 2.0 and later, which covers .NET console applications:

```bash
dotnet new console -o pubnub-quickstart
cd pubnub-quickstart
dotnet add package PubnubPCL
```

Then open the generated `Program.cs`.

### Go

Make sure you have Go 1.25 or later installed, then create a module and add the PubNub Go SDK:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
go mod init pubnub-quickstart
go get github.com/pubnub/go/v9
```

Then create a file called `main.go` in that directory.

### Rust

Make sure you have Rust and Cargo installed, then create a binary crate and add the PubNub Rust SDK along with the async runtime and JSON crates this tutorial uses:

```bash
cargo new pubnub-quickstart
cd pubnub-quickstart
cargo add pubnub tokio --features tokio/full
cargo add futures serde_json
```

Then open the generated `src/main.rs`.

### C-Core (legacy)

:::note New SDK available
C-Core (legacy) is still supported. If you are starting a new project, use the new [C SDK](https://www.pubnub.com/docs/sdks/c.md).
:::

The PubNub C-Core (legacy) SDK has no package to install. You clone the repository and build a static library from it. This tutorial uses the OpenSSL build, which gives you TLS, so you need OpenSSL and `pkg-config` available.

Clone the repository and build the library for the synchronous interface:

```bash
git clone https://github.com/pubnub/c-core.git
cd c-core/openssl
make -f posix.mk pubnub_sync.a
```

Then create a file called `main.c` in the `c-core/openssl` directory. Keeping it there means the compiler finds `pubnub_sync.h` and the built library without extra path configuration.

### Swift

Make sure you have Xcode installed, then create a Swift package:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
swift package init --type executable
```

Replace the generated `Package.swift` with the following, which adds the PubNub Swift SDK as a dependency:

```swift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "pubnub-quickstart",
    platforms: [.macOS(.v11)],
    dependencies: [
        .package(url: "https://github.com/pubnub/swift.git", from: "10.2.0")
    ],
    targets: [
        .executableTarget(
            name: "pubnub-quickstart",
            dependencies: [.product(name: "PubNubSDK", package: "swift")]
        )
    ]
)
```

Delete the generated `Sources/pubnub-quickstart/pubnub_quickstart.swift` and create `Sources/pubnub-quickstart/main.swift` in its place. The name `main.swift` is what allows top-level code in the file.

### Objective-C

The PubNub Objective-C SDK is distributed as a CocoaPods pod, so you build this program in Xcode.

1. Make sure you have Xcode and [CocoaPods](https://guides.cocoapods.org/using/getting-started.html) installed. Install CocoaPods with `gem install cocoapods` if you don't have it.
2. In Xcode, select **File** > **New** > **Project**, choose **macOS** > **Command Line Tool**, set **Product Name** to `pubnub-quickstart` and **Language** to **Objective-C**, and save the project.
3. Close Xcode.
4. In the project directory, create a `Podfile`:

   ```ruby
   platform :osx, '11.0'

   target 'pubnub-quickstart' do
     pod 'PubNub', '~> 7.0.4'
   end
   ```

5. Install the pod:

   ```bash
   pod install
   ```

6. Open `pubnub-quickstart.xcworkspace`. From now on, use the workspace rather than the `.xcodeproj` file.

You'll edit the `main.m` file that Xcode generated.

### Dart

Make sure you have the Dart SDK installed, then create a package and add the PubNub Dart SDK. PubNub must run inside a Dart package, not as a standalone script file:

```bash
dart create pubnub_quickstart
cd pubnub_quickstart
dart pub add pubnub
```

Then open the generated `bin/pubnub_quickstart.dart`.

### PHP

Make sure you have PHP and [Composer](https://getcomposer.org/) installed, then create a directory and require the PubNub PHP SDK:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
composer require pubnub/pubnub
```

Then create a file called `index.php` in that directory.

### Ruby

The PubNub Ruby SDK requires Ruby 3.0 or later. Install the gem and create a directory to work in:

```bash
mkdir pubnub-quickstart
cd pubnub-quickstart
gem install pubnub
```

Then create a file called `app.rb` in that directory.

### Unity

The PubNub Unity SDK is a Unity package, and you configure your keys through a scriptable object in the editor rather than in code.

1. In Unity Hub, create a new Unity project.
2. In the editor, open **Window** > **Package Manager**, click **+**, and select **Add package from git URL**.
3. Paste `https://github.com/pubnub/unity.git?path=/PubNubUnity/Assets/PubNub` and click **Add**.
4. In the **Project** window, right-click the `Assets` folder and select **Create** > **PubNub** > **PubNub Config Asset**. This creates a `PNConfigAsset` scriptable object that holds your keys.
5. Select the new `PNConfigAsset` and set **Publish Key** and **Subscribe Key** to your keys from the Admin Portal. Set **Log Level** to **None** so the Console shows only your own messages rather than the SDK's internal logs.
6. In the `Assets` folder, create a C# script called `PubNubQuickstart`.

You wire the script to a game object after you write it, in the [Run the program](#run-the-program) step.

### Unreal Engine

The PubNub Unreal Engine SDK is a plugin, and you configure your keys in **Project Settings** rather than in code. This tutorial uses C++.

1. Launch Unreal Engine 5.2 or later and create a new **C++** project called `Quickstart`.
2. Install the **PubNub Gaming SDK** [plugin](https://www.fab.com/listings/9501a8d6-f9e6-4cf8-8b56-d173bdb71fc4) from FAB and enable **PubNubSDK** in your project.
3. In `Source/Quickstart/Quickstart.Build.cs`, add the PubNub module dependency:

   ```cpp
   PrivateDependencyModuleNames.AddRange(new string[] { "PubnubLibrary" });
   ```

4. Select **Edit** > **Project Settings**, scroll to the **Plugins** section, and click **Pubnub SDK**. Set **Publish Key** and **Subscribe Key** to your keys from the Admin Portal, and leave **Initialize Automatically** checked so the plugin initializes itself.
5. Select **Tools** > **New C++ Class...**, inherit from **GameInstance**, and name the class `PubNubQuickstartGameInstance`.

You set the class as the project's default game instance after you write it, in the [Run the program](#run-the-program) step.

## Initialize PubNub

### JavaScript

Open `index.js` and add the following:

```javascript
const PubNub = require('pubnub');

const pubnub = new PubNub({
  publishKey: 'YOUR_PUBLISH_KEY',
  subscribeKey: 'YOUR_SUBSCRIBE_KEY',
  userId: 'quickstart-user',
});
```

### Python

Open `main.py` and add the following:

```python
import threading

from pubnub.callbacks import SubscribeCallback
from pubnub.enums import PNStatusCategory
from pubnub.exceptions import PubNubException
from pubnub.pnconfiguration import PNConfiguration
from pubnub.pubnub import PubNub

config = PNConfiguration()
config.publish_key = 'YOUR_PUBLISH_KEY'
config.subscribe_key = 'YOUR_SUBSCRIBE_KEY'
config.user_id = 'quickstart-user'

pubnub = PubNub(config)
```

### Java

Open `src/main/java/App.java` and add the following:

```java
import com.google.gson.JsonObject;
import com.pubnub.api.PubNubException;
import com.pubnub.api.UserId;
import com.pubnub.api.enums.PNStatusCategory;
import com.pubnub.api.java.PubNub;
import com.pubnub.api.java.v2.PNConfiguration;
import com.pubnub.api.java.v2.callbacks.StatusListener;
import com.pubnub.api.java.v2.entities.Channel;
import com.pubnub.api.java.v2.subscriptions.Subscription;
import com.pubnub.api.models.consumer.PNStatus;

public class App {
    public static void main(String[] args) throws PubNubException, InterruptedException {
        PNConfiguration config = PNConfiguration.builder(new UserId("quickstart-user"), "YOUR_SUBSCRIBE_KEY")
                .publishKey("YOUR_PUBLISH_KEY")
                .build();

        PubNub pubnub = PubNub.create(config);
    }
}
```

The subscribe key is a constructor argument on the configuration builder, alongside the `userId`. The publish key is a separate builder method.

### Kotlin

Open `src/main/kotlin/Main.kt` and add the following:

```kotlin
import com.google.gson.JsonObject
import com.pubnub.api.PubNub
import com.pubnub.api.UserId
import com.pubnub.api.enums.PNStatusCategory
import com.pubnub.api.models.consumer.PNStatus
import com.pubnub.api.v2.PNConfiguration
import com.pubnub.api.v2.callbacks.StatusListener
import com.pubnub.api.v2.entities.Channel

fun main() {
    val config = PNConfiguration.builder(UserId("quickstart-user"), "YOUR_SUBSCRIBE_KEY").apply {
        publishKey = "YOUR_PUBLISH_KEY"
    }.build()

    val pubnub = PubNub.create(config)
}
```

The subscribe key is a constructor argument on the configuration builder, alongside the `userId`. The publish key is set on the builder itself.

### C#

Replace the contents of `Program.cs` with the following:

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PubnubApi;

PNConfiguration config = new PNConfiguration(new UserId("quickstart-user"))
{
    PublishKey = "YOUR_PUBLISH_KEY",
    SubscribeKey = "YOUR_SUBSCRIBE_KEY"
};

Pubnub pubnub = new Pubnub(config);
```

### Go

Open `main.go` and add the following:

```go
package main

import (
	"fmt"
	"log"

	pubnub "github.com/pubnub/go/v9"
)

func main() {
	config := pubnub.NewConfigWithUserId(pubnub.UserId("quickstart-user"))
	config.PublishKey = "YOUR_PUBLISH_KEY"
	config.SubscribeKey = "YOUR_SUBSCRIBE_KEY"

	pn := pubnub.NewPubNub(config)
}
```

### Rust

Replace the contents of `src/main.rs` with the following:

```rust
use futures::StreamExt;
use pubnub::subscribe::{ConnectionStatus, EventEmitter, EventSubscriber, Subscriber};
use pubnub::{Keyset, PubNubClientBuilder};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pubnub = PubNubClientBuilder::with_reqwest_transport()
        .with_keyset(Keyset {
            publish_key: Some("YOUR_PUBLISH_KEY"),
            subscribe_key: "YOUR_SUBSCRIBE_KEY",
            secret_key: None,
        })
        .with_user_id("quickstart-user")
        .build()?;

    Ok(())
}
```

### C-Core (legacy)

Open `main.c` and add the following:

```c
#include <stdio.h>

#include "pubnub_sync.h"

int main(void)
{
    pubnub_t *pubnub = pubnub_alloc();

    if (NULL == pubnub) {
        puts("Failed to allocate a PubNub context");
        return -1;
    }

    pubnub_init(pubnub, "YOUR_PUBLISH_KEY", "YOUR_SUBSCRIBE_KEY");
    pubnub_set_user_id(pubnub, "quickstart-user");

    pubnub_free(pubnub);

    return 0;
}
```

C-Core (legacy) uses dynamic allocation for its contexts. Always create a context with `pubnub_alloc()`, check the return value, and release it with `pubnub_free()`.

`pubnub_sync.h` selects the synchronous interface, where each PubNub call runs to completion before the next line executes.

### Swift

Open `Sources/pubnub-quickstart/main.swift` and add the following:

```swift
import Foundation
import PubNubSDK

let pubnub = PubNub(configuration: PubNubConfiguration(
    publishKey: "YOUR_PUBLISH_KEY",
    subscribeKey: "YOUR_SUBSCRIBE_KEY",
    userId: "quickstart-user"
))
```

### Objective-C

Open `main.m` and replace its contents with the following:

```objectivec
#import <Foundation/Foundation.h>
#import <PubNub/PubNub.h>

@interface QuickstartListener : NSObject <PNEventsListener>
@property(nonatomic, strong) PubNub *client;
@end

@implementation QuickstartListener
@end

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        PNConfiguration *configuration = [PNConfiguration configurationWithPublishKey:@"YOUR_PUBLISH_KEY"
                                                                        subscribeKey:@"YOUR_SUBSCRIBE_KEY"
                                                                              userID:@"quickstart-user"];

        QuickstartListener *listener = [QuickstartListener new];
        listener.client = [PubNub clientWithConfiguration:configuration];
    }
    return 0;
}
```

`QuickstartListener` holds a strong reference to the client so it isn't released while the program runs, and it adopts `PNEventsListener`, the protocol through which the SDK delivers messages and connection status.

### Dart

Replace the contents of `bin/pubnub_quickstart.dart` with the following:

```dart
import 'package:pubnub/pubnub.dart';

Future<void> main() async {
  final pubnub = PubNub(
    defaultKeyset: Keyset(
      publishKey: 'YOUR_PUBLISH_KEY',
      subscribeKey: 'YOUR_SUBSCRIBE_KEY',
      userId: UserId('quickstart-user'),
    ),
  );
}
```

### PHP

Open `index.php` and add the following:

```php
<?php

require_once 'vendor/autoload.php';

use PubNub\Callbacks\SubscribeCallback;
use PubNub\Enums\PNStatusCategory;
use PubNub\Exceptions\PubNubException;
use PubNub\PNConfiguration;
use PubNub\PubNub;

$config = new PNConfiguration();
$config->setPublishKey('YOUR_PUBLISH_KEY');
$config->setSubscribeKey('YOUR_SUBSCRIBE_KEY');
$config->setUserId('quickstart-user');

$pubnub = new PubNub($config);
```

### Ruby

Open `app.rb` and add the following:

```ruby
require 'pubnub'

pubnub = Pubnub.new(
  publish_key: 'YOUR_PUBLISH_KEY',
  subscribe_key: 'YOUR_SUBSCRIBE_KEY',
  user_id: 'quickstart-user'
)
```

### Unity

Open `PubNubQuickstart.cs` and replace its contents with the following:

```csharp
using System.Collections.Generic;
using PubnubApi;
using PubnubApi.Unity;
using UnityEngine;

public class PubNubQuickstart : PNManagerBehaviour
{
    private Subscription subscription;

    private void Awake()
    {
        Initialize("quickstart-user");
    }
}
```

`PNManagerBehaviour` is the SDK's base `MonoBehaviour`. `Initialize()` reads your keys from the `PNConfigAsset` you assign in the inspector and creates the `pubnub` client, so your keys never appear in code. You pass the `userId` here because it identifies this client rather than the keyset.

### Unreal Engine

Open `PubNubQuickstartGameInstance.h` and replace its contents with the following:

```cpp
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "PubnubSubsystem.h"
#include "PubNubQuickstartGameInstance.generated.h"

UCLASS()
class QUICKSTART_API UPubNubQuickstartGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	virtual void Init() override;

	UFUNCTION()
	void OnMessageReceived(FPubnubMessageData MessageData);

	UFUNCTION()
	void OnSubscriptionStatusChanged(EPubnubSubscriptionStatus Status, FPubnubSubscriptionStatusData StatusData);

private:
	UPubnubSubsystem* PubnubSubsystem = nullptr;
};
```

Replace `QUICKSTART_API` with `[YOUR_PROJECT_NAME]_API` if your project isn't called `Quickstart`.

Open `PubNubQuickstartGameInstance.cpp` and replace its contents with the following:

```cpp
#include "PubNubQuickstartGameInstance.h"

void UPubNubQuickstartGameInstance::Init()
{
	Super::Init();

	PubnubSubsystem = GetSubsystem<UPubnubSubsystem>();
	PubnubSubsystem->SetUserID(TEXT("quickstart-user"));
}
```

`UPubnubSubsystem` is a game instance subsystem, so you get it from the game instance rather than constructing a client. Your keys come from **Project Settings**, so the only thing you set in code is the `userId` that identifies this client.

Replace `YOUR_PUBLISH_KEY` and `YOUR_SUBSCRIBE_KEY` with the keys from your keyset, wherever this tutorial shows them. The `userId` is a string that identifies this client on the PubNub network. Use any unique value for now.

## Subscribe to a channel

### JavaScript

Append this to `index.js`:

```javascript
const channel = pubnub.channel('quickstart-channel');
const subscription = channel.subscription();

subscription.onMessage = (event) => {
  console.log('Message received:', event.message.text);
};

pubnub.addListener({
  status: (event) => {
    if (event.category === 'PNConnectedCategory') {
      publishMessage();
    }
  },
});

subscription.subscribe();
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it. `subscription.onMessage` fires each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`pubnub.addListener()` reports connection status at the client level. When status reports `PNConnectedCategory`, the listener calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Python

Append this to `main.py`:

```python
connected = threading.Event()

class StatusListener(SubscribeCallback):
    def status(self, pubnub, status):
        if status.category == PNStatusCategory.PNConnectedCategory:
            connected.set()

pubnub.add_listener(StatusListener())

subscription = pubnub.channel('quickstart-channel').subscription()

def on_message(message):
    print('Message received:', message.message['text'])

subscription.on_message = on_message

subscription.subscribe()
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it. `subscription.on_message` fires each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`subscribe()` runs the connection on a background thread and returns immediately. Connection status arrives through a `SubscribeCallback` added to the client, so the listener sets a `threading.Event` that the main thread waits on before publishing.

### Java

Add this inside `main()`, after creating the `pubnub` client:

```java
        Channel channel = pubnub.channel("quickstart-channel");
        Subscription subscription = channel.subscription();

        subscription.setOnMessage(event ->
                System.out.println("Message received: "
                        + event.getMessage().getAsJsonObject().get("text").getAsString()));

        pubnub.addListener(new StatusListener() {
            @Override
            public void status(PubNub pubnub, PNStatus status) {
                if (status.getCategory() == PNStatusCategory.PNConnectedCategory) {
                    publishMessage(channel);
                }
            }
        });

        subscription.subscribe();
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it. The handler you pass to `setOnMessage()` runs each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

A `StatusListener` on the client reports connection status. When status reports `PNConnectedCategory`, the listener calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Kotlin

Add this inside `main()`, after creating the `pubnub` client:

```kotlin
    val channel = pubnub.channel("quickstart-channel")
    val subscription = channel.subscription()

    subscription.onMessage = { event ->
        println("Message received: ${event.message.asJsonObject["text"].asString}")
    }

    pubnub.addListener(object : StatusListener {
        override fun status(pubnub: PubNub, status: PNStatus) {
            if (status.category == PNStatusCategory.PNConnectedCategory) {
                publishMessage(channel)
            }
        }
    })

    subscription.subscribe()
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it. `subscription.onMessage` fires each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

A `StatusListener` on the client reports connection status. When status reports `PNConnectedCategory`, the listener calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### C#

Append this to `Program.cs`:

```csharp
Subscription subscription = pubnub.Channel("quickstart-channel").Subscription();

subscription.onMessage += (Pubnub pn, PNMessageResult<object> message) =>
{
    Dictionary<string, object> payload =
        pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(message.Message.ToString());
    Console.WriteLine($"Message received: {payload["text"]}");
};

pubnub.AddListener(new SubscribeCallbackExt(
    delegate (Pubnub pn, PNStatus status)
    {
        if (status.Category == PNStatusCategory.PNConnectedCategory)
        {
            _ = PublishMessage();
        }
    }));

subscription.Subscribe<object>();
```

`pubnub.Channel()` creates a channel entity, and `.Subscription()` scopes a subscription to it. `subscription.onMessage` fires each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

Keep the `subscription` variable in scope for as long as you want to receive updates. If the garbage collector reclaims a subscription, that subscription stops delivering events.

A `SubscribeCallbackExt` added to the client reports connection status. When status reports `PNConnectedCategory`, the listener calls `PublishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Go

Add this inside `main()`, after creating the `pn` client:

```go
	listener := pubnub.NewListener()

	go func() {
		for {
			select {
			case status := <-listener.Status:
				if status.Category == pubnub.PNConnectedCategory {
					publishMessage(pn)
				}
			case message := <-listener.Message:
				payload := message.Message.(map[string]interface{})
				fmt.Println("Message received:", payload["text"])
			}
		}
	}()

	pn.AddListener(listener)

	pn.Subscribe().
		Channels([]string{"quickstart-channel"}).
		Execute()
```

The Go SDK delivers events on channels rather than through callbacks. `listener.Message` receives messages and `listener.Status` receives connection status, so a goroutine selects over both. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

When status reports `PNConnectedCategory`, the goroutine calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Rust

Add this inside `main()`, after building the `pubnub` client and before `Ok(())`:

```rust
    let subscription = pubnub.channel("quickstart-channel").subscription(None);
    subscription.subscribe();

    tokio::spawn(
        subscription
            .messages_stream()
            .for_each(|message| async move {
                let payload: Value = serde_json::from_slice(&message.data).unwrap();
                println!("Message received: {}", payload["text"].as_str().unwrap());
            }),
    );

    let mut status_stream = pubnub.status_stream();
    while let Some(status) = status_stream.next().await {
        if matches!(status, ConnectionStatus::Connected) {
            break;
        }
    }
```

`pubnub.channel()` creates a channel entity, and `.subscription(None)` scopes a subscription to it with no extra options. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

The Rust SDK exposes events as streams. `messages_stream()` yields messages, so a spawned task prints each one as it arrives. `pubnub.status_stream()` yields connection status, and the loop waits on it until the status is `Connected`, so the publish you add in the next step runs only once the subscription is confirmed active.

Keep the `subscription` binding alive for as long as you want to receive updates. Dropping it unsubscribes.

### C-Core (legacy)

The synchronous interface has no callbacks. A `pubnub_subscribe()` call returns when PubNub responds, and the first call is what establishes the connection.

Add this inside `main()`, after `pubnub_set_user_id()`:

```c
    if (PNR_STARTED == pubnub_subscribe(pubnub, "quickstart-channel", NULL)) {
        pubnub_await(pubnub);
    }
```

`pubnub_subscribe()` starts the transaction and `pubnub_await()` blocks until it finishes. This first call returns no messages. It connects to `quickstart-channel` and records the point in time from which the next call reads, so any message published after it is delivered to the next `pubnub_subscribe()` call. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

### Swift

Append this to `main.swift`:

```swift
let subscription = pubnub.channel("quickstart-channel").subscription()

subscription.onMessage = { message in
    if let text = message.payload[rawValue: "text"] as? String {
        print("Message received: \(text)")
    }
}

pubnub.onConnectionStateChange = { newStatus in
    if case .connected = newStatus {
        publishMessage()
    }
}

subscription.subscribe()
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it. `subscription.onMessage` fires each time a message arrives on that channel. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`pubnub.onConnectionStateChange` reports connection status at the client level. When the status becomes `.connected`, the handler calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Objective-C

Add these two methods to the `QuickstartListener` implementation, between `@implementation` and `@end`:

```objectivec
- (void)client:(PubNub *)client didReceiveMessage:(PNMessageResult *)message {
    NSLog(@"Message received: %@", message.data.message[@"text"]);
}

- (void)client:(PubNub *)client didReceiveStatus:(PNStatus *)status {
    if (status.category == PNConnectedCategory) {
        [self publishMessage];
    }
}
```

Then add this inside `main()`, after creating the client:

```objectivec
        [listener.client addListener:listener];
        [listener.client subscribeToChannels:@[@"quickstart-channel"] withPresence:NO];
```

The `PNEventsListener` protocol delivers both messages and connection status to the object you register with `addListener:`. `didReceiveMessage:` fires each time a message arrives, and `didReceiveStatus:` fires on connection changes. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

When the status category is `PNConnectedCategory`, the listener calls `publishMessage`, which you add in the next step. That way the message publishes only once the subscription is confirmed active. Passing `withPresence:NO` keeps this program to messages only.

### Dart

Add this inside `main()`, after creating the `pubnub` client:

```dart
  final subscription = pubnub.subscribe(channels: {'quickstart-channel'});

  subscription.messages.listen((envelope) {
    print('Message received: ${envelope.payload['text']}');
  });

  await subscription.whenStarts;
```

`pubnub.subscribe()` returns a subscription that exposes incoming events as streams. Listening to `subscription.messages` runs your callback for each message that arrives on the channel. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`subscription.whenStarts` is a future that completes once the subscription is connected and listening, so awaiting it means the publish you add in the next step runs only after the channel is live.

### PHP

Append this to `index.php`:

```php
class QuickstartListener extends SubscribeCallback
{
    public function status($pubnub, $status)
    {
        if ($status->getCategory() === PNStatusCategory::PNConnectedCategory) {
            publishMessage($pubnub);
        }
    }

    public function message($pubnub, $message)
    {
        echo 'Message received: ' . $message->getMessage()['text'] . PHP_EOL;
    }

    public function presence($pubnub, $presence)
    {
    }
}

$pubnub->addListener(new QuickstartListener());

$pubnub->subscribe()->channels('quickstart-channel')->execute();
```

A `SubscribeCallback` subclass receives messages and connection status. `message()` runs each time a message arrives on the channel, and `status()` runs on connection changes. `presence()` is required by the base class, so it stays empty here. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`execute()` blocks and runs the subscribe loop, so it must be the last statement your script runs. When the status category is `PNConnectedCategory`, the listener calls `publishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Ruby

Append this to `app.rb`:

```ruby
callback = Pubnub::SubscribeCallback.new(
  message: ->(envelope) {
    puts "Message received: #{envelope.result[:data][:message]['text']}"
  },
  presence: ->(envelope) {},
  status: ->(envelope) {
    publish_message(pubnub) if envelope.status[:category] == Pubnub::Constants::STATUS_ACK
  }
)

pubnub.add_listener(callback: callback)

pubnub.subscribe(channels: ['quickstart-channel'])
```

A `Pubnub::SubscribeCallback` holds one lambda per event type. The `message` lambda runs each time a message arrives on the channel. The `presence` lambda is required, so it stays empty here. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

`subscribe` runs the connection on a background thread and returns immediately. The `status` lambda receives one acknowledgment, `Pubnub::Constants::STATUS_ACK`, when the subscribe loop connects, and it calls `publish_message`, which you add in the next step. That way the message publishes only once the subscription is confirmed active.

### Unity

Add the subscription and the listeners to `PubNubQuickstart.cs`, so the class looks like this:

```csharp
public class PubNubQuickstart : PNManagerBehaviour
{
    private Subscription subscription;

    private void Awake()
    {
        listener.onStatus += OnStatus;
        listener.onMessage += OnMessage;

        Initialize("quickstart-user");

        subscription = pubnub.Channel("quickstart-channel").Subscription();
        subscription.Subscribe<object>();
    }

    private void OnStatus(Pubnub pn, PNStatus status)
    {
        if (status.Category == PNStatusCategory.PNConnectedCategory)
        {
            PublishMessage();
        }
    }

    private void OnMessage(Pubnub pn, PNMessageResult<object> result)
    {
        Dictionary<string, object> payload =
            pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(result.Message.ToString());
        Debug.Log($"Message received: {payload["text"]}");
    }

    protected override void OnDestroy()
    {
        listener.onStatus -= OnStatus;
        listener.onMessage -= OnMessage;

        base.OnDestroy();
    }
}
```

`pubnub.Channel()` creates a channel entity, and `.Subscription()` scopes a subscription to it. Attach your handlers to the inherited `listener` before you call `Initialize()`. The Unity SDK dispatches those handlers on the main game loop, so you can touch scene objects and call `Debug.Log()` from them directly. For more on entities and subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

When status reports `PNConnectedCategory`, `OnStatus` calls `PublishMessage()`, which you add in the next step. That way the message publishes only once the subscription is confirmed active. `OnDestroy` detaches the handlers when the object goes away.

### Unreal Engine

Add the listener bindings and the subscribe call to `Init()` in `PubNubQuickstartGameInstance.cpp`, then add the two handlers:

```cpp
void UPubNubQuickstartGameInstance::Init()
{
	Super::Init();

	PubnubSubsystem = GetSubsystem<UPubnubSubsystem>();
	PubnubSubsystem->SetUserID(TEXT("quickstart-user"));

	PubnubSubsystem->OnMessageReceived.AddDynamic(this, &UPubNubQuickstartGameInstance::OnMessageReceived);
	PubnubSubsystem->OnSubscriptionStatusChanged.AddDynamic(this, &UPubNubQuickstartGameInstance::OnSubscriptionStatusChanged);

	PubnubSubsystem->SubscribeToChannel(TEXT("quickstart-channel"));
}

void UPubNubQuickstartGameInstance::OnMessageReceived(FPubnubMessageData MessageData)
{
	UE_LOG(LogTemp, Log, TEXT("Message received: %s"), *MessageData.Message);
}
```

`OnMessageReceived` and `OnSubscriptionStatusChanged` are dynamic multicast delegates on the subsystem, so you bind your `UFUNCTION()` handlers with `AddDynamic`. `MessageData.Message` holds the message payload as an `FString`. For more on subscriptions, refer to [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md).

Channels are created implicitly the first time they are used and do not require provisioning, so `quickstart-channel` becomes available the moment you use it.

## Publish a message

### JavaScript

Append this to `index.js`:

```javascript
async function publishMessage() {
  try {
    await pubnub.publish({
      channel: 'quickstart-channel',
      message: { text: 'Hello, PubNub!' },
    });
  } catch (error) {
    console.error('Publish failed:', error);
  }
}
```

`publish()` sends a message to a channel. The message can be any JSON-serializable value. Here it's a plain object with a `text` field.

### Python

Append this to `main.py`:

```python
connected.wait()

try:
    pubnub.publish() \
        .channel('quickstart-channel') \
        .message({'text': 'Hello, PubNub!'}) \
        .sync()
except PubNubException as error:
    print('Publish failed:', error)
```

`publish()` sends a message to a channel, and `sync()` runs the request and blocks until it completes. The message can be any JSON-serializable value. Here it's a dictionary with a `text` key.

`connected.wait()` blocks the main thread until the status listener reports the connection, so the publish never races ahead of the subscription.

### Java

Add this method to the `App` class, after `main()`:

```java
    static void publishMessage(Channel channel) {
        JsonObject message = new JsonObject();
        message.addProperty("text", "Hello, PubNub!");

        channel.publish(message).async(result ->
                result.onFailure(exception ->
                        System.out.println("Publish failed: " + exception.getMessage())));
    }
```

`publish()` sends a message to a channel, and `async()` runs the request without blocking. The message can be any JSON-serializable value. Here it's a Gson `JsonObject` with a `text` property.

### Kotlin

Add this function to `Main.kt`, after `main()`:

```kotlin
fun publishMessage(channel: Channel) {
    val message = JsonObject().apply { addProperty("text", "Hello, PubNub!") }

    channel.publish(message).async { result ->
        result.onFailure { exception -> println("Publish failed: ${exception.message}") }
    }
}
```

`publish()` sends a message to a channel, and `async()` runs the request without blocking. The message can be any JSON-serializable value. Here it's a Gson `JsonObject` with a `text` property.

### C#

Append this to `Program.cs`:

```csharp
async Task PublishMessage()
{
    PNResult<PNPublishResult> result = await pubnub.Publish()
        .Channel("quickstart-channel")
        .Message(new Dictionary<string, object> { { "text", "Hello, PubNub!" } })
        .ExecuteAsync();

    if (result.Status.Error)
    {
        Console.WriteLine($"Publish failed: {result.Status.ErrorData.Information}");
    }
}
```

`Publish()` sends a message to a channel, and `ExecuteAsync()` awaits the result. The message can be any JSON-serializable value. Here it's a dictionary with a `text` key.

Local [functions](https://www.pubnub.com/docs/message-processing/serverless/overview.md) can appear after the statements that call them, so this can go at the end of the file.

### Go

Add this function to `main.go`, after `main()`:

```go
func publishMessage(pn *pubnub.PubNub) {
	_, _, err := pn.Publish().
		Channel("quickstart-channel").
		Message(map[string]interface{}{"text": "Hello, PubNub!"}).
		Execute()
	if err != nil {
		log.Println("Publish failed:", err)
	}
}
```

`Publish()` sends a message to a channel, and `Execute()` runs the request and returns when it completes. The message can be any JSON-serializable value. Here it's a map with a `text` key.

### Rust

Add this inside `main()`, after the status loop and before `Ok(())`:

```rust
    pubnub
        .publish_message(json!({ "text": "Hello, PubNub!" }))
        .channel("quickstart-channel")
        .execute()
        .await?;
```

`publish_message()` sends a message to a channel, and `execute()` returns a future you await. The message can be any serializable value. Here it's a `serde_json` value with a `text` field.

### C-Core (legacy)

Add this inside `main()`, after the first `pubnub_subscribe()` block:

```c
    if (PNR_STARTED == pubnub_publish(pubnub, "quickstart-channel", "{\"text\":\"Hello, PubNub!\"}")) {
        pubnub_await(pubnub);
    }
```

`pubnub_publish()` sends a message to a channel, and `pubnub_await()` blocks until PubNub acknowledges it. C-Core (legacy) takes the message as a JSON string that you build yourself, so the quotes inside it are escaped.

One context runs one transaction at a time. Publishing here is safe because the subscribe transaction above already finished.

### Swift

Append this to `main.swift`:

```swift
func publishMessage() {
    pubnub.publish(channel: "quickstart-channel", message: ["text": "Hello, PubNub!"]) { result in
        if case let .failure(error) = result {
            print("Publish failed: \(error.localizedDescription)")
        }
    }
}
```

`publish(channel:message:)` sends a message to a channel and reports the outcome in its completion handler. The message can be any JSON-serializable value. Here it's a dictionary with a `text` key.

### Objective-C

Add this method to the `QuickstartListener` implementation, alongside the other two:

```objectivec
- (void)publishMessage {
    [self.client publish:@{@"text": @"Hello, PubNub!"}
               toChannel:@"quickstart-channel"
          withCompletion:^(PNPublishStatus *status) {
        if (status.isError) {
            NSLog(@"Publish failed: %@", status.errorData.information);
        }
    }];
}
```

`publish:toChannel:withCompletion:` sends a message to a channel and reports the outcome in its completion block. The message can be any JSON-serializable value. Here it's an `NSDictionary` with a `text` key.

### Dart

Add this inside `main()`, after `await subscription.whenStarts`:

```dart
  try {
    await pubnub.publish('quickstart-channel', {'text': 'Hello, PubNub!'});
  } catch (error) {
    print('Publish failed: $error');
  }
```

`publish()` sends a message to a channel and returns a future you await. The message can be any JSON-serializable value. Here it's a map with a `text` key.

### PHP

Append this to `index.php`:

```php
function publishMessage($pubnub)
{
    try {
        $pubnub->publish()
            ->channel('quickstart-channel')
            ->message(['text' => 'Hello, PubNub!'])
            ->sync();
    } catch (PubNubException $error) {
        echo 'Publish failed: ' . $error->getMessage() . PHP_EOL;
    }
}
```

`publish()` sends a message to a channel, and `sync()` runs the request and blocks until it completes. The message can be any JSON-serializable value. Here it's an array with a `text` key.

PHP makes top-level functions available before the line that declares them, so this can go after the `subscribe()` call that uses it.

### Ruby

Append this to `app.rb`:

```ruby
def publish_message(pubnub)
  pubnub.publish(
    channel: 'quickstart-channel',
    message: { text: 'Hello, PubNub!' }
  ) do |envelope|
    puts "Publish failed: #{envelope.status[:category]}" if envelope.status[:error]
  end
end
```

`publish` sends a message to a channel and reports the outcome in the block you pass it. The message can be any JSON-serializable value. Here it's a hash with a `:text` key.

### Unity

Add this method to the `PubNubQuickstart` class:

```csharp
    private async void PublishMessage()
    {
        PNResult<PNPublishResult> result = await pubnub.Publish()
            .Channel("quickstart-channel")
            .Message(new Dictionary<string, object> { { "text", "Hello, PubNub!" } })
            .ExecuteAsync();

        if (result.Status.Error)
        {
            Debug.LogError($"Publish failed: {result.Status.ErrorData.Information}");
        }
    }
```

`Publish()` sends a message to a channel, and `ExecuteAsync()` awaits the result without blocking the game loop. The message can be any JSON-serializable value. Here it's a dictionary with a `text` key.

### Unreal Engine

Add this handler to `PubNubQuickstartGameInstance.cpp`:

```cpp
void UPubNubQuickstartGameInstance::OnSubscriptionStatusChanged(EPubnubSubscriptionStatus Status, FPubnubSubscriptionStatusData StatusData)
{
	if (Status == EPubnubSubscriptionStatus::PSS_Connected)
	{
		PubnubSubsystem->PublishMessage(TEXT("quickstart-channel"), TEXT("{\"text\":\"Hello, PubNub!\"}"));
	}
}
```

`PublishMessage()` sends a message to a channel. The Unreal SDK takes the message as a JSON `FString` that you build yourself, so the quotes inside it are escaped.

Publishing from `OnSubscriptionStatusChanged` when the status is `PSS_Connected` means the message goes out only once the subscription is confirmed active.

## Run the program

### JavaScript

```bash
node index.js
```

You should see:

```text
Message received: Hello, PubNub!
```

The program keeps running because the subscription holds the connection open. Press **Ctrl+C** to stop it.

### Python

```bash
python main.py
```

You should see:

```text
Message received: Hello, PubNub!
```

The program keeps running because the subscription thread holds the connection open. Press **Ctrl+C** to stop it.

### Java

Add this as the last statement in `main()`, so the program doesn't exit while the subscription is still running:

```java
        Thread.currentThread().join();
```

Then run the program:

```bash
mvn -q compile exec:java -Dexec.mainClass=App
```

You should see:

```text
Message received: Hello, PubNub!
```

The SDK logs through SLF4J, so a few `SLF4J:` notices about a missing logger binding may appear before that line. They are harmless.

`Thread.currentThread().join()` never returns, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### Kotlin

Add this as the last statement in `main()`, so the program doesn't exit while the subscription is still running:

```kotlin
    Thread.currentThread().join()
```

Then run the program:

```bash
gradle run -q
```

You should see:

```text
Message received: Hello, PubNub!
```

The SDK logs through SLF4J, so a few `SLF4J:` notices about a missing logger binding may appear before that line. They are harmless.

`Thread.currentThread().join()` never returns, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### C#

Add this line to `Program.cs`, after `subscription.Subscribe<object>()` and before the `PublishMessage` local function, so the program doesn't exit while the subscription is still running:

```csharp
await Task.Delay(-1);
```

Then run the program:

```bash
dotnet run
```

You should see:

```text
Message received: Hello, PubNub!
```

`Task.Delay(-1)` waits forever, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### Go

Add this as the last statement in `main()`, so the program doesn't exit while the subscription is still running:

```go
	select {}
```

Then run the program:

```bash
go run main.go
```

You should see:

```text
Message received: Hello, PubNub!
```

An empty `select {}` blocks forever, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### Rust

Add this before `Ok(())` in `main()`, so the program doesn't exit while the subscription is still running:

```rust
    std::future::pending::<()>().await;
```

Then run the program:

```bash
cargo run
```

You should see:

```text
Message received: Hello, PubNub!
```

`std::future::pending()` never completes, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### C-Core (legacy)

Add this second `pubnub_subscribe()` call and the read loop inside `main()`, after the publish and before `pubnub_free()`:

```c
    if (PNR_STARTED == pubnub_subscribe(pubnub, "quickstart-channel", NULL)) {
        pubnub_await(pubnub);
    }

    for (char const *message = pubnub_get(pubnub); NULL != message; message = pubnub_get(pubnub)) {
        printf("Message received: %s\n", message);
    }
```

This second `pubnub_subscribe()` call returns as soon as PubNub has a message for you. `pubnub_get()` then returns the messages one at a time and `NULL` when there are none left.

Compile and run the program from the `c-core/openssl` directory:

```bash
cc -I. -I.. -o pubnub-quickstart main.c pubnub_sync.a $(pkg-config --libs openssl)
./pubnub-quickstart
```

You should see:

```text
Message received: {"text":"Hello, PubNub!"}
```

C-Core (legacy) hands you the raw JSON payload rather than a parsed object, so the whole message prints. Unlike the other languages on this page, this program then exits on its own, because the synchronous interface reads messages when you ask for them instead of holding a listener open.

### Swift

Add this as the last line of `main.swift`, so the program doesn't exit while the subscription is still running:

```swift
RunLoop.main.run()
```

Then run the program:

```bash
swift run
```

You should see:

```text
Message received: Hello, PubNub!
```

`RunLoop.main.run()` never returns, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### Objective-C

Add this as the last statement inside the `@autoreleasepool` block in `main()`, so the program doesn't exit while the subscription is still running:

```objectivec
        [[NSRunLoop currentRunLoop] run];
```

Then build and run the program in Xcode with **⌘R**.

In the Xcode console you should see:

```text
Message received: Hello, PubNub!
```

`NSLog` prefixes each line with a timestamp and the process name, so the line appears after that prefix.

The run loop never returns, so the program keeps running with the connection open. Press the **Stop** button in Xcode to end it.

### Dart

```bash
dart run
```

You should see:

```text
Message received: Hello, PubNub!
```

The program keeps running after `main()` returns because the subscription stream is still active. Press **Ctrl+C** to stop it.

### PHP

```bash
php index.php
```

You should see:

```text
Message received: Hello, PubNub!
```

The program keeps running because `execute()` runs the subscribe loop indefinitely. Press **Ctrl+C** to stop it.

### Ruby

Add this as the last line of `app.rb`, so the program doesn't exit while the subscription is still running:

```ruby
sleep
```

Then run the program:

```bash
ruby app.rb
```

You should see:

```text
Message received: Hello, PubNub!
```

`sleep` with no argument blocks forever, so the program keeps running with the connection open. Press **Ctrl+C** to stop it.

### Unity

Wire the script into your scene, then play it:

1. Create an empty game object in your scene and drag `PubNubQuickstart` onto it to add it as a component.
2. Drag your `PNConfigAsset` onto the **Pn Configuration** field of the **PubNubQuickstart (Script)** component.
3. Enter Play mode.

In the **Console** window you should see:

```text
Message received: Hello, PubNub!
```

If the Console shows `PNConfigAsset is missing` instead, the config asset is not assigned to the component.

The subscription stays open for as long as you remain in Play mode. Exit Play mode to stop it.

### Unreal Engine

Make your game instance the one the project uses, then play it:

1. Select **Edit** > **Project Settings** and open **Maps & Modes**.
2. Set **Game Instance Class** to `PubNubQuickstartGameInstance`.
3. Compile the project and click **Play**.

In the **Output Log** you should see:

```text
Message received: {"text":"Hello, PubNub!"}
```

The Unreal SDK hands you the raw JSON payload rather than a parsed object, so the whole message prints.

The subscription stays open for as long as the game instance lives. Stop playing to end it.

The message travels from your program to the PubNub network and back to the same subscriber.

## Troubleshooting

If the output does not appear:

* Verify your keys. Publish keys start with `pub-c-` and subscribe keys start with `sub-c-`. Swapping them is a common mistake.
* Check for Access Manager errors. If Access Manager is enabled on your keyset, the SDK requires a valid token to subscribe and publish. If your status handler reports an access-denied status, disable Access Manager on your keyset in the [Admin Portal](https://admin.pubnub.com) or refer to [Access Manager](https://www.pubnub.com/docs/security/access-control/overview.md).
* Check your network. If the SDK cannot reach PubNub, your status handler reports a connection failure instead of a connected status, so the publish step never runs. Verify that the machine has internet access and that outbound HTTPS to `*.pubnub.com`, `*.pndsn.com`, `*.pubnub.net`, and `*.pubnubapi.com` isn't blocked by a firewall. If your network policy requires IP allowlisting instead of domain allowlisting, that requires a paid plan. [Contact support](https://support.pubnub.com/hc/en-us/requests/new) for the current IP list.

## What happened

Your program acted as both publisher and subscriber on `quickstart-channel`:

1. You created a subscription to `quickstart-channel` and attached a handler for incoming messages.
2. You activated the subscription, which opened a connection to PubNub and started listening.
3. Once the subscription was active, your program published a message to the same channel.
4. PubNub routed the message to every active subscriber on `quickstart-channel`, including this program.
5. Your message handler ran and printed the text.

This is the pub/sub model in practice: publishers send messages to a channel, and PubNub delivers them to all current subscribers.

## Complete file

### JavaScript

```javascript
const PubNub = require('pubnub');

const pubnub = new PubNub({
  publishKey: 'YOUR_PUBLISH_KEY',
  subscribeKey: 'YOUR_SUBSCRIBE_KEY',
  userId: 'quickstart-user',
});

const channel = pubnub.channel('quickstart-channel');
const subscription = channel.subscription();

subscription.onMessage = (event) => {
  console.log('Message received:', event.message.text);
};

pubnub.addListener({
  status: (event) => {
    if (event.category === 'PNConnectedCategory') {
      publishMessage();
    }
  },
});

subscription.subscribe();

async function publishMessage() {
  try {
    await pubnub.publish({
      channel: 'quickstart-channel',
      message: { text: 'Hello, PubNub!' },
    });
  } catch (error) {
    console.error('Publish failed:', error);
  }
}
```

### Python

```python
import threading

from pubnub.callbacks import SubscribeCallback
from pubnub.enums import PNStatusCategory
from pubnub.exceptions import PubNubException
from pubnub.pnconfiguration import PNConfiguration
from pubnub.pubnub import PubNub

config = PNConfiguration()
config.publish_key = 'YOUR_PUBLISH_KEY'
config.subscribe_key = 'YOUR_SUBSCRIBE_KEY'
config.user_id = 'quickstart-user'

pubnub = PubNub(config)

connected = threading.Event()

class StatusListener(SubscribeCallback):
    def status(self, pubnub, status):
        if status.category == PNStatusCategory.PNConnectedCategory:
            connected.set()

pubnub.add_listener(StatusListener())

subscription = pubnub.channel('quickstart-channel').subscription()

def on_message(message):
    print('Message received:', message.message['text'])

subscription.on_message = on_message

subscription.subscribe()

connected.wait()

try:
    pubnub.publish() \
        .channel('quickstart-channel') \
        .message({'text': 'Hello, PubNub!'}) \
        .sync()
except PubNubException as error:
    print('Publish failed:', error)
```

### Java

```java
import com.google.gson.JsonObject;
import com.pubnub.api.PubNubException;
import com.pubnub.api.UserId;
import com.pubnub.api.enums.PNStatusCategory;
import com.pubnub.api.java.PubNub;
import com.pubnub.api.java.v2.PNConfiguration;
import com.pubnub.api.java.v2.callbacks.StatusListener;
import com.pubnub.api.java.v2.entities.Channel;
import com.pubnub.api.java.v2.subscriptions.Subscription;
import com.pubnub.api.models.consumer.PNStatus;

public class App {
    public static void main(String[] args) throws PubNubException, InterruptedException {
        PNConfiguration config = PNConfiguration.builder(new UserId("quickstart-user"), "YOUR_SUBSCRIBE_KEY")
                .publishKey("YOUR_PUBLISH_KEY")
                .build();

        PubNub pubnub = PubNub.create(config);

        Channel channel = pubnub.channel("quickstart-channel");
        Subscription subscription = channel.subscription();

        subscription.setOnMessage(event ->
                System.out.println("Message received: "
                        + event.getMessage().getAsJsonObject().get("text").getAsString()));

        pubnub.addListener(new StatusListener() {
            @Override
            public void status(PubNub pubnub, PNStatus status) {
                if (status.getCategory() == PNStatusCategory.PNConnectedCategory) {
                    publishMessage(channel);
                }
            }
        });

        subscription.subscribe();

        Thread.currentThread().join();
    }

    static void publishMessage(Channel channel) {
        JsonObject message = new JsonObject();
        message.addProperty("text", "Hello, PubNub!");

        channel.publish(message).async(result ->
                result.onFailure(exception ->
                        System.out.println("Publish failed: " + exception.getMessage())));
    }
}
```

### Kotlin

```kotlin
import com.google.gson.JsonObject
import com.pubnub.api.PubNub
import com.pubnub.api.UserId
import com.pubnub.api.enums.PNStatusCategory
import com.pubnub.api.models.consumer.PNStatus
import com.pubnub.api.v2.PNConfiguration
import com.pubnub.api.v2.callbacks.StatusListener
import com.pubnub.api.v2.entities.Channel

fun main() {
    val config = PNConfiguration.builder(UserId("quickstart-user"), "YOUR_SUBSCRIBE_KEY").apply {
        publishKey = "YOUR_PUBLISH_KEY"
    }.build()

    val pubnub = PubNub.create(config)

    val channel = pubnub.channel("quickstart-channel")
    val subscription = channel.subscription()

    subscription.onMessage = { event ->
        println("Message received: ${event.message.asJsonObject["text"].asString}")
    }

    pubnub.addListener(object : StatusListener {
        override fun status(pubnub: PubNub, status: PNStatus) {
            if (status.category == PNStatusCategory.PNConnectedCategory) {
                publishMessage(channel)
            }
        }
    })

    subscription.subscribe()

    Thread.currentThread().join()
}

fun publishMessage(channel: Channel) {
    val message = JsonObject().apply { addProperty("text", "Hello, PubNub!") }

    channel.publish(message).async { result ->
        result.onFailure { exception -> println("Publish failed: ${exception.message}") }
    }
}
```

### C#

```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PubnubApi;

PNConfiguration config = new PNConfiguration(new UserId("quickstart-user"))
{
    PublishKey = "YOUR_PUBLISH_KEY",
    SubscribeKey = "YOUR_SUBSCRIBE_KEY"
};

Pubnub pubnub = new Pubnub(config);

Subscription subscription = pubnub.Channel("quickstart-channel").Subscription();

subscription.onMessage += (Pubnub pn, PNMessageResult<object> message) =>
{
    Dictionary<string, object> payload =
        pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(message.Message.ToString());
    Console.WriteLine($"Message received: {payload["text"]}");
};

pubnub.AddListener(new SubscribeCallbackExt(
    delegate (Pubnub pn, PNStatus status)
    {
        if (status.Category == PNStatusCategory.PNConnectedCategory)
        {
            _ = PublishMessage();
        }
    }));

subscription.Subscribe<object>();

await Task.Delay(-1);

async Task PublishMessage()
{
    PNResult<PNPublishResult> result = await pubnub.Publish()
        .Channel("quickstart-channel")
        .Message(new Dictionary<string, object> { { "text", "Hello, PubNub!" } })
        .ExecuteAsync();

    if (result.Status.Error)
    {
        Console.WriteLine($"Publish failed: {result.Status.ErrorData.Information}");
    }
}
```

### Go

```go
package main

import (
	"fmt"
	"log"

	pubnub "github.com/pubnub/go/v9"
)

func main() {
	config := pubnub.NewConfigWithUserId(pubnub.UserId("quickstart-user"))
	config.PublishKey = "YOUR_PUBLISH_KEY"
	config.SubscribeKey = "YOUR_SUBSCRIBE_KEY"

	pn := pubnub.NewPubNub(config)

	listener := pubnub.NewListener()

	go func() {
		for {
			select {
			case status := <-listener.Status:
				if status.Category == pubnub.PNConnectedCategory {
					publishMessage(pn)
				}
			case message := <-listener.Message:
				payload := message.Message.(map[string]interface{})
				fmt.Println("Message received:", payload["text"])
			}
		}
	}()

	pn.AddListener(listener)

	pn.Subscribe().
		Channels([]string{"quickstart-channel"}).
		Execute()

	select {}
}

func publishMessage(pn *pubnub.PubNub) {
	_, _, err := pn.Publish().
		Channel("quickstart-channel").
		Message(map[string]interface{}{"text": "Hello, PubNub!"}).
		Execute()
	if err != nil {
		log.Println("Publish failed:", err)
	}
}
```

### Rust

```rust
use futures::StreamExt;
use pubnub::subscribe::{ConnectionStatus, EventEmitter, EventSubscriber, Subscriber};
use pubnub::{Keyset, PubNubClientBuilder};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let pubnub = PubNubClientBuilder::with_reqwest_transport()
        .with_keyset(Keyset {
            publish_key: Some("YOUR_PUBLISH_KEY"),
            subscribe_key: "YOUR_SUBSCRIBE_KEY",
            secret_key: None,
        })
        .with_user_id("quickstart-user")
        .build()?;

    let subscription = pubnub.channel("quickstart-channel").subscription(None);
    subscription.subscribe();

    tokio::spawn(
        subscription
            .messages_stream()
            .for_each(|message| async move {
                let payload: Value = serde_json::from_slice(&message.data).unwrap();
                println!("Message received: {}", payload["text"].as_str().unwrap());
            }),
    );

    let mut status_stream = pubnub.status_stream();
    while let Some(status) = status_stream.next().await {
        if matches!(status, ConnectionStatus::Connected) {
            break;
        }
    }

    pubnub
        .publish_message(json!({ "text": "Hello, PubNub!" }))
        .channel("quickstart-channel")
        .execute()
        .await?;

    std::future::pending::<()>().await;
    Ok(())
}
```

### C-Core (legacy)

```c
#include <stdio.h>

#include "pubnub_sync.h"

int main(void)
{
    pubnub_t *pubnub = pubnub_alloc();

    if (NULL == pubnub) {
        puts("Failed to allocate a PubNub context");
        return -1;
    }

    pubnub_init(pubnub, "YOUR_PUBLISH_KEY", "YOUR_SUBSCRIBE_KEY");
    pubnub_set_user_id(pubnub, "quickstart-user");

    if (PNR_STARTED == pubnub_subscribe(pubnub, "quickstart-channel", NULL)) {
        pubnub_await(pubnub);
    }

    if (PNR_STARTED == pubnub_publish(pubnub, "quickstart-channel", "{\"text\":\"Hello, PubNub!\"}")) {
        pubnub_await(pubnub);
    }

    if (PNR_STARTED == pubnub_subscribe(pubnub, "quickstart-channel", NULL)) {
        pubnub_await(pubnub);
    }

    for (char const *message = pubnub_get(pubnub); NULL != message; message = pubnub_get(pubnub)) {
        printf("Message received: %s\n", message);
    }

    pubnub_free(pubnub);

    return 0;
}
```

### Swift

```swift
import Foundation
import PubNubSDK

let pubnub = PubNub(configuration: PubNubConfiguration(
    publishKey: "YOUR_PUBLISH_KEY",
    subscribeKey: "YOUR_SUBSCRIBE_KEY",
    userId: "quickstart-user"
))

let subscription = pubnub.channel("quickstart-channel").subscription()

subscription.onMessage = { message in
    if let text = message.payload[rawValue: "text"] as? String {
        print("Message received: \(text)")
    }
}

pubnub.onConnectionStateChange = { newStatus in
    if case .connected = newStatus {
        publishMessage()
    }
}

subscription.subscribe()

func publishMessage() {
    pubnub.publish(channel: "quickstart-channel", message: ["text": "Hello, PubNub!"]) { result in
        if case let .failure(error) = result {
            print("Publish failed: \(error.localizedDescription)")
        }
    }
}

RunLoop.main.run()
```

### Objective-C

```objectivec
#import <Foundation/Foundation.h>
#import <PubNub/PubNub.h>

@interface QuickstartListener : NSObject <PNEventsListener>
@property(nonatomic, strong) PubNub *client;
@end

@implementation QuickstartListener

- (void)client:(PubNub *)client didReceiveMessage:(PNMessageResult *)message {
    NSLog(@"Message received: %@", message.data.message[@"text"]);
}

- (void)client:(PubNub *)client didReceiveStatus:(PNStatus *)status {
    if (status.category == PNConnectedCategory) {
        [self publishMessage];
    }
}

- (void)publishMessage {
    [self.client publish:@{@"text": @"Hello, PubNub!"}
               toChannel:@"quickstart-channel"
          withCompletion:^(PNPublishStatus *status) {
        if (status.isError) {
            NSLog(@"Publish failed: %@", status.errorData.information);
        }
    }];
}

@end

int main(int argc, const char *argv[]) {
    @autoreleasepool {
        PNConfiguration *configuration = [PNConfiguration configurationWithPublishKey:@"YOUR_PUBLISH_KEY"
                                                                        subscribeKey:@"YOUR_SUBSCRIBE_KEY"
                                                                              userID:@"quickstart-user"];

        QuickstartListener *listener = [QuickstartListener new];
        listener.client = [PubNub clientWithConfiguration:configuration];

        [listener.client addListener:listener];
        [listener.client subscribeToChannels:@[@"quickstart-channel"] withPresence:NO];

        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}
```

### Dart

```dart
import 'package:pubnub/pubnub.dart';

Future<void> main() async {
  final pubnub = PubNub(
    defaultKeyset: Keyset(
      publishKey: 'YOUR_PUBLISH_KEY',
      subscribeKey: 'YOUR_SUBSCRIBE_KEY',
      userId: UserId('quickstart-user'),
    ),
  );

  final subscription = pubnub.subscribe(channels: {'quickstart-channel'});

  subscription.messages.listen((envelope) {
    print('Message received: ${envelope.payload['text']}');
  });

  await subscription.whenStarts;

  try {
    await pubnub.publish('quickstart-channel', {'text': 'Hello, PubNub!'});
  } catch (error) {
    print('Publish failed: $error');
  }
}
```

### PHP

```php
<?php

require_once 'vendor/autoload.php';

use PubNub\Callbacks\SubscribeCallback;
use PubNub\Enums\PNStatusCategory;
use PubNub\Exceptions\PubNubException;
use PubNub\PNConfiguration;
use PubNub\PubNub;

$config = new PNConfiguration();
$config->setPublishKey('YOUR_PUBLISH_KEY');
$config->setSubscribeKey('YOUR_SUBSCRIBE_KEY');
$config->setUserId('quickstart-user');

$pubnub = new PubNub($config);

class QuickstartListener extends SubscribeCallback
{
    public function status($pubnub, $status)
    {
        if ($status->getCategory() === PNStatusCategory::PNConnectedCategory) {
            publishMessage($pubnub);
        }
    }

    public function message($pubnub, $message)
    {
        echo 'Message received: ' . $message->getMessage()['text'] . PHP_EOL;
    }

    public function presence($pubnub, $presence)
    {
    }
}

$pubnub->addListener(new QuickstartListener());

$pubnub->subscribe()->channels('quickstart-channel')->execute();

function publishMessage($pubnub)
{
    try {
        $pubnub->publish()
            ->channel('quickstart-channel')
            ->message(['text' => 'Hello, PubNub!'])
            ->sync();
    } catch (PubNubException $error) {
        echo 'Publish failed: ' . $error->getMessage() . PHP_EOL;
    }
}
```

### Ruby

```ruby
require 'pubnub'

pubnub = Pubnub.new(
  publish_key: 'YOUR_PUBLISH_KEY',
  subscribe_key: 'YOUR_SUBSCRIBE_KEY',
  user_id: 'quickstart-user'
)

callback = Pubnub::SubscribeCallback.new(
  message: ->(envelope) {
    puts "Message received: #{envelope.result[:data][:message]['text']}"
  },
  presence: ->(envelope) {},
  status: ->(envelope) {
    publish_message(pubnub) if envelope.status[:category] == Pubnub::Constants::STATUS_ACK
  }
)

pubnub.add_listener(callback: callback)

pubnub.subscribe(channels: ['quickstart-channel'])

def publish_message(pubnub)
  pubnub.publish(
    channel: 'quickstart-channel',
    message: { text: 'Hello, PubNub!' }
  ) do |envelope|
    puts "Publish failed: #{envelope.status[:category]}" if envelope.status[:error]
  end
end

sleep
```

### Unity

```csharp
using System.Collections.Generic;
using PubnubApi;
using PubnubApi.Unity;
using UnityEngine;

public class PubNubQuickstart : PNManagerBehaviour
{
    private Subscription subscription;

    private void Awake()
    {
        listener.onStatus += OnStatus;
        listener.onMessage += OnMessage;

        Initialize("quickstart-user");

        subscription = pubnub.Channel("quickstart-channel").Subscription();
        subscription.Subscribe<object>();
    }

    private void OnStatus(Pubnub pn, PNStatus status)
    {
        if (status.Category == PNStatusCategory.PNConnectedCategory)
        {
            PublishMessage();
        }
    }

    private void OnMessage(Pubnub pn, PNMessageResult<object> result)
    {
        Dictionary<string, object> payload =
            pubnub.JsonPluggableLibrary.DeserializeToDictionaryOfObject(result.Message.ToString());
        Debug.Log($"Message received: {payload["text"]}");
    }

    private async void PublishMessage()
    {
        PNResult<PNPublishResult> result = await pubnub.Publish()
            .Channel("quickstart-channel")
            .Message(new Dictionary<string, object> { { "text", "Hello, PubNub!" } })
            .ExecuteAsync();

        if (result.Status.Error)
        {
            Debug.LogError($"Publish failed: {result.Status.ErrorData.Information}");
        }
    }

    protected override void OnDestroy()
    {
        listener.onStatus -= OnStatus;
        listener.onMessage -= OnMessage;

        base.OnDestroy();
    }
}
```

### Unreal Engine

`PubNubQuickstartGameInstance.h`:

```cpp
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "PubnubSubsystem.h"
#include "PubNubQuickstartGameInstance.generated.h"

UCLASS()
class QUICKSTART_API UPubNubQuickstartGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	virtual void Init() override;

	UFUNCTION()
	void OnMessageReceived(FPubnubMessageData MessageData);

	UFUNCTION()
	void OnSubscriptionStatusChanged(EPubnubSubscriptionStatus Status, FPubnubSubscriptionStatusData StatusData);

private:
	UPubnubSubsystem* PubnubSubsystem = nullptr;
};
```

`PubNubQuickstartGameInstance.cpp`:

```cpp
#include "PubNubQuickstartGameInstance.h"

void UPubNubQuickstartGameInstance::Init()
{
	Super::Init();

	PubnubSubsystem = GetSubsystem<UPubnubSubsystem>();
	PubnubSubsystem->SetUserID(TEXT("quickstart-user"));

	PubnubSubsystem->OnMessageReceived.AddDynamic(this, &UPubNubQuickstartGameInstance::OnMessageReceived);
	PubnubSubsystem->OnSubscriptionStatusChanged.AddDynamic(this, &UPubNubQuickstartGameInstance::OnSubscriptionStatusChanged);

	PubnubSubsystem->SubscribeToChannel(TEXT("quickstart-channel"));
}

void UPubNubQuickstartGameInstance::OnMessageReceived(FPubnubMessageData MessageData)
{
	UE_LOG(LogTemp, Log, TEXT("Message received: %s"), *MessageData.Message);
}

void UPubNubQuickstartGameInstance::OnSubscriptionStatusChanged(EPubnubSubscriptionStatus Status, FPubnubSubscriptionStatusData StatusData)
{
	if (Status == EPubnubSubscriptionStatus::PSS_Connected)
	{
		PubnubSubsystem->PublishMessage(TEXT("quickstart-channel"), TEXT("{\"text\":\"Hello, PubNub!\"}"));
	}
}
```

## Next steps

You've published and received a message with PubNub. From here, you can go deeper into pub/sub or start adding other capabilities to your application:

* [Pub/Sub overview](https://www.pubnub.com/docs/pub-sub/overview.md). Learn about signals, channel groups, and publish options.
* [Presence](https://www.pubnub.com/docs/presence/overview.md). Track who is connected to a channel in real time.
* [Data storage](https://www.pubnub.com/docs/data-storage/overview.md). Retrieve messages that subscribers missed while offline.
* [Available SDKs](https://www.pubnub.com/docs/getting-started/available-sdks.md). Find the SDK for your platform or language.

Last updated at: 2026-09-30T07:20:08.000Z
