---
source_url: https://www.pubnub.com/docs/data-storage/metadata/events
title: App Context events
updated_at: 2026-09-30T07:20:08.000Z
---

# App Context events

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

:::note Starting a new app? Use DataSync
[DataSync](https://www.pubnub.com/docs/data-storage/structured-data/overview.md) is the successor to App Context. It does everything App Context does for users, channels, and memberships, and adds typed schemas, partial updates with ETags, field-level access control, and per-class expiry. Refer to [How DataSync compares to App Context](https://www.pubnub.com/docs/data-storage/structured-data/overview.md) for a feature-by-feature comparison.
DataSync is currently available to new accounts and to accounts that are not actively using App Context. If your keysets already use App Context, keep using it for now. App Context remains fully supported and these pages stay accurate.
:::

An App Context event is the real-time notification
[App Context](https://www.pubnub.com/docs/data-storage/metadata/overview.md)
sends when a user's, channel's, or membership's stored metadata changes.

It travels through the same subscription and [event listener](https://www.pubnub.com/docs/pub-sub/subscribe/event-listeners.md) infrastructure as a message or a [presence](https://www.pubnub.com/docs/presence/overview.md) update, arriving through a handler PubNub's docs call `onObjects`. This page explains:

* the fields the payload carries, and how they differ from the raw event PubNub puts on the wire
* why the payload only ever reports `set` or `delete`, never a separate "created"
* why the payload's entity-type field reads `uuid` instead of `user`
* how the payload's `data` differs between a user, a channel, and a membership event
* why one SDK delivers every App Context event to a single handler while another splits it into several
* how the server-side events Events & Actions generates from the same change relate to this one

Receiving any of this requires App Context enabled on your keyset, and the matching **User Metadata Events**, **Channel Metadata Events**, or **Membership Events** toggle turned on for that entity type. For where each event type lands and how to turn those toggles on, refer to [Real-time updates when metadata changes](https://www.pubnub.com/docs/data-storage/metadata/overview.md#real-time-updates-when-metadata-changes). For the procedure that registers a handler, refer to [Event listeners](https://www.pubnub.com/docs/pub-sub/subscribe/event-listeners.md).

## SDKs unwrap the event before your handler sees it

An App Context event starts life as an ordinary message on the wire. It's [internally typed](https://www.pubnub.com/docs/pub-sub/overview.md#message-types-categorize-traffic-on-a-shared-channel), so a client can tell it apart from a regular published message before inspecting the payload. That raw form nests the App Context-specific fields inside a `message` object, alongside a `source` and `version` marker that identify the API that produced it:

```json
{
  "channel": "my_channel",
  "message": {
    "source": "objects",
    "version": "2.0",
    "event": "set",
    "type": "channel",
    "data": { "id": "my_channel", "name": "Main channel" }
  },
  "subscription": "my_channel",
  "timetoken": "17511946699655811"
}
```

Most SDKs flatten this before your handler sees it, promoting `event`, `type`, and `data` to top-level fields alongside the channel, subscription match, and timetoken:

| Field | Description |
| --- | --- |
| `channel` | The channel the event was delivered on |
| `subscription` | The channel group or wildcard subscription match, if any |
| `timetoken` | When the change was recorded |
| `publisher` | The [User ID](https://www.pubnub.com/docs/architecture/core-concepts.md#user-id) that made the change |
| `event` | `set` or `delete` |
| `type` | Which entity changed: `uuid`, `channel`, or `membership` |
| `data` | The metadata that changed |

Not every SDK flattens or exposes every field the same way.

JavaScript keeps the App Context payload under `event.message`. In an `onObjects` handler, read `event.message.event`, `event.message.type`, and `event.message.data`. The delivery metadata stays at the top level in `event.channel`, `event.subscription`, and `event.timetoken`.

Field names and types are per SDK, not platform-wide. For the exact shape your handler receives, refer to the API reference for your platform in [Available SDKs](https://www.pubnub.com/docs/sdks.md).

## An event only ever says set or delete

`event` has exactly two values, and neither one distinguishes a brand-new record from a change to an existing one. Setting a user's metadata for the first time and updating it a week later both arrive as `set`. A subscriber that needs to tell "this user just joined" from "this user's profile changed" has to keep its own record of what it already knew. The event alone doesn't say.

That's a deliberate difference from the server-side events [Events & Actions](https://www.pubnub.com/docs/integrations/event-forwarding/overview.md) generates from the same underlying change, which do separate a first `set` from a later one. Refer to [Client-side and server-side events observe the same change independently](#client-side-and-server-side-events-observe-the-same-change-independently) below.

## Why type says uuid

A change to a user's metadata reports `type: "uuid"` rather than `type: "user"`. The value still identifies the same entity.

## The data field's shape follows the entity

`data` carries a different record depending on what `type` says changed. The following illustrates a `set` event for each entity type, matching the record shape the corresponding get call returns:

### uuid

```json
{
  "channel": "test-user-1",
  "subscription": "test-user-1",
  "timetoken": "17511946699655811",
  "publisher": "test-user-1",
  "event": "set",
  "type": "uuid",
  "data": {
    "id": "test-user-1",
    "name": "John Doe",
    "email": "johndoe@pubnub.com",
    "custom": null,
    "updated": "2019-02-20T23:11:20.893755",
    "eTag": "MDcyQ0REOTUtNEVBOC00QkY2LTgwOUUtNDkwQzI4MjgzMTcwCg=="
  }
}
```

### channel

```json
{
  "channel": "team.blue",
  "subscription": "team.blue",
  "timetoken": "17511946699812340",
  "publisher": "test-user-1",
  "event": "set",
  "type": "channel",
  "data": {
    "id": "team.blue",
    "name": "Blue Team",
    "description": "The channel for Blue team and no other teams.",
    "custom": null,
    "updated": "2019-02-20T23:11:20.893755",
    "eTag": "RTc1NUQwNUItREMyNy00Q0YxLUJCNDItMEZDMTZDMzVCN0VGCg=="
  }
}
```

### membership

```json
{
  "channel": "team.blue",
  "subscription": "team.blue",
  "timetoken": "17511947001234567",
  "publisher": "test-user-1",
  "event": "set",
  "type": "membership",
  "data": {
    "channel": { "id": "team.blue" },
    "uuid": { "id": "test-user-1" },
    "custom": { "starred": false },
    "updated": "2019-02-20T23:11:20.893755",
    "eTag": "RUNDMDUwNjktNUYwRC00RTI0LUI1M0QtNUUzNkE2NkU0MEVFCg=="
  }
}
```

A `delete` event's `data` carries little or nothing, since the record it refers to no longer exists to describe. Field names inside `data` are per SDK like everything else on this page. Check your platform's API reference before relying on one literally.

## One handler, or several, depending on your SDK

Every other client-side event type gets [one dedicated handler per type](https://www.pubnub.com/docs/pub-sub/subscribe/event-listeners.md#one-handler-per-event-type). App Context is the exception some SDKs make. Because one change can be a user, a channel, or a membership, an SDK hands it to you in one of three ways:

* **One combined handler.** A single handler receives every entity type, and your code branches on `type` itself. Kotlin, C#, Swift, Objective-C, and Unity all follow this pattern.
* **Several separate handlers.** One handler per entity, so your code never checks `type` at all. Java, Go, and Python all split this way, though the mechanism differs.
* **The same handler as any other message.** Dart and PHP deliver an App Context event through the same generic handler you'd use for an ordinary published message. Your code has to inspect the payload itself to tell the two apart.

None of these change what data reaches you, only how many places you write code to receive it. The following shows each SDK's own listener code.

### JavaScript

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

subscription.onObjects = (event) => {
  const { event: action, type, data } = event.message;

  if (type === 'uuid') {
    console.log('User metadata event:', action, data);
  } else if (type === 'channel') {
    console.log('Channel metadata event:', action, data);
  } else if (type === 'membership') {
    console.log('Membership event:', action, data);
  }
};

subscription.subscribe();
```

### Python

```python
class PrintListener(SubscribeCallback):
    def message(self, pubnub, message):
        print('Message received:', message.message)

    def uuid(self, pubnub, event):
        print('User metadata event:', event)

    def channel(self, pubnub, event):
        print('Channel metadata event:', event)

    def membership(self, pubnub, event):
        print('Membership event:', event)

subscription = pubnub.channel('channel_1').subscription()
subscription.add_listener(PrintListener())
subscription.subscribe()
```

### Java

```java
subscription.setOnMessage((PNMessageResult pnMessageResult) -> {
    JsonElement message = pnMessageResult.getMessage();
    String channel1 = pnMessageResult.getChannel();
    String publisher = pnMessageResult.getPublisher();
    String subscription1 = pnMessageResult.getSubscription();
    String customMessageType = pnMessageResult.getCustomMessageType();
    Long timetoken = pnMessageResult.getTimetoken();
});

subscription.setOnSignal((PNSignalResult pnSignalResult) -> {
    JsonElement message = pnSignalResult.getMessage();
    String channel = pnSignalResult.getChannel();
    String publisher = pnSignalResult.getPublisher();
    String subscription1 = pnSignalResult.getSubscription();
    String customMessageType = pnSignalResult.getCustomMessageType();
    Long timetoken = pnSignalResult.getTimetoken();
});

subscription.setOnMessageAction((PNMessageActionResult pnMessageActionResult) -> {
    PNMessageAction messageAction = pnMessageActionResult.getMessageAction();
    String channel = pnMessageActionResult.getChannel();
    String event = pnMessageActionResult.getEvent();
    String publisher = pnMessageActionResult.getPublisher();
    String subscription1 = pnMessageActionResult.getSubscription();
    Long timetoken = pnMessageActionResult.getTimetoken();
});

subscription.setOnFile((PNFileEventResult pnFileEventResult) -> {
    JsonElement message = (JsonElement) pnFileEventResult.getMessage();
    PNDownloadableFile file = pnFileEventResult.getFile();
    String channel = pnFileEventResult.getChannel();
    String publisher = pnFileEventResult.getPublisher();
    String subscription1 = pnFileEventResult.getSubscription();
    String customMessageType = pnFileEventResult.getCustomMessageType();
    Long timetoken = pnFileEventResult.getTimetoken();
});

subscription.setOnUuidMetadata((PNUUIDMetadataResult pnUUIDMetadataResult) -> {
    String event = pnUUIDMetadataResult.getEvent();
    PNUUIDMetadata data = pnUUIDMetadataResult.getData();
    String channel = pnUUIDMetadataResult.getChannel();
    String publisher = pnUUIDMetadataResult.getPublisher();
    String subscription1 = pnUUIDMetadataResult.getSubscription();
    Long timetoken = pnUUIDMetadataResult.getTimetoken();
});

subscription.setOnChannelMetadata((PNChannelMetadataResult pnChannelMetadataResult) -> {
    String event = pnChannelMetadataResult.getEvent();
    PNChannelMetadata data = pnChannelMetadataResult.getData();
    String channel = pnChannelMetadataResult.getChannel();
    String publisher = pnChannelMetadataResult.getPublisher();
    String subscription1 = pnChannelMetadataResult.getSubscription();
    Long timetoken = pnChannelMetadataResult.getTimetoken();
});

subscription.setOnMembership((PNMembershipResult pnMembershipResult) -> {
    String event = pnMembershipResult.getEvent();
    PNMembership data = pnMembershipResult.getData();
    String channel = pnMembershipResult.getChannel();
    String publisher = pnMembershipResult.getPublisher();
    String subscription1 = pnMembershipResult.getSubscription();
    Long timetoken = pnMembershipResult.getTimetoken();
});

subscription.setOnPresence((PNPresenceEventResult pnPresenceEventResult) -> {
    String event = pnPresenceEventResult.getEvent();
    Integer occupancy = pnPresenceEventResult.getOccupancy();
    String channel = pnPresenceEventResult.getChannel();
    List<String> join = pnPresenceEventResult.getJoin();
    List<String> leave = pnPresenceEventResult.getLeave();
    JsonElement state = pnPresenceEventResult.getState();
    List<String> timeout = pnPresenceEventResult.getTimeout();
    String subscription1 = pnPresenceEventResult.getSubscription();
    Long timetoken = pnPresenceEventResult.getTimetoken();
});
```

`setOnUuidMetadata`, `setOnChannelMetadata`, and `setOnMembership` are the three separate handlers. Nothing here checks a `type` field, because each method only ever receives its own entity.

### Kotlin

```kotlin
subscription.onMessage = { message ->
    // Handle message
}

subscription.onSignal = { signal ->
    // Handle signal
}

subscription.onMessageAction = { messageAction ->
    // Handle message reaction
}

subscription.onFile = { file ->
    // Handle file event
}

subscription.onObjects = { obj ->
    // Handle metadata updates
}

subscription.onPresence = { presence ->
    // Handle presence updates
}

val onMessage: (PNMessageResult) -> Unit = { /* Handle message */ }
val onSignal: (PNSignalResult) -> Unit = { /* Handle signal */ }
val onMessageAction: (PNMessageActionResult) -> Unit = { /* Handle message reaction */ }
val onFile: (PNFileEventResult) -> Unit = { /* Handle file event */ }
val onObjects: (PNObjectEventResult) -> Unit = { /* Handle metadata updates */ }
val onPresence: (PNPresenceEventResult) -> Unit = { /* Handle presence updates */ }

subscription.onMessage = onMessage
subscription.onSignal = onSignal
subscription.onMessageAction = onMessageAction
subscription.onFile = onFile
subscription.onObjects = onObjects
subscription.onPresence = onPresence
```

`subscription.onObjects` is the one combined handler. Its `PNObjectEventResult` carries a `type` field to tell a user, channel, or membership change apart.

### C#

```csharp
// Adding listener.
pubnub.AddListener(new SubscribeCallbackExt(
    delegate(Pubnub pnObj, PNMessageResult<object> pubMsg)
    {
        Console.WriteLine(pubnub.JsonPluggableLibrary.SerializeToJsonString(pubMsg));
        var channelName = pubMsg.Channel;
        var channelGroupName = pubMsg.Subscription;
        var pubTT = pubMsg.Timetoken;
        var msg = pubMsg.Message;
        var publisher = pubMsg.Publisher;
    },
    delegate(Pubnub pnObj, PNPresenceEventResult presenceEvnt)
    {
        Console.WriteLine(pubnub.JsonPluggableLibrary.SerializeToJsonString(presenceEvnt));
        var action = presenceEvnt.Event; // Can be join, leave, state-change or timeout
        var channelName = presenceEvnt.Channel; // The channel for which the message belongs
        var occupancy = presenceEvnt.Occupancy; // No. of users connected with the channel
        var state = presenceEvnt.State; // User State
        var channelGroupName =
            presenceEvnt.Subscription; //  The channel group or wildcard subscription match (if exists)
        var publishTime = presenceEvnt.Timestamp; // Publish timetoken
        var timetoken = presenceEvnt.Timetoken; // Current timetoken
        var uuid = presenceEvnt.Uuid; // UUIDs of users who are connected with the channel
    },
    delegate(Pubnub pnObj, PNSignalResult<object> signalMsg)
    {
        Console.WriteLine(pubnub.JsonPluggableLibrary.SerializeToJsonString(signalMsg));
        var channelName = signalMsg.Channel; // The channel for which the signal belongs
        var channelGroupName =
            signalMsg.Subscription; // The channel group or wildcard subscription match (if exists)
        var pubTT = signalMsg.Timetoken; // Publish timetoken
        var msg = signalMsg.Message; // The Payload
        var publisher = signalMsg.Publisher; //The Publisher
    },
    delegate(Pubnub pnObj, PNObjectEventResult objectEventObj)
    {
        var channelName = objectEventObj.Channel; // Channel
        var channelMetadata = objectEventObj.ChannelMetadata; //Channel Metadata
        var uidMetadata = objectEventObj.UuidMetadata; // UUID metadata
        var evnt = objectEventObj.Event; // Event
        var type = objectEventObj.Type; // Event type
        if (objectEventObj.Type == "uuid")
        {
            /* got uuid metadata related event. */
        }
        else if (objectEventObj.Type == "channel")
        {
            /* got channel metadata related event. */
        }
        else if (objectEventObj.Type == "membership")
        {
            /* got membership related event. */
        }

        Console.WriteLine(pubnub.JsonPluggableLibrary.SerializeToJsonString(objectEventObj));
    },
    delegate(Pubnub pnObj, PNMessageActionEventResult msgActionEvent)
    {
        //handle message action
        var channelName = msgActionEvent.Channel; // The channel for which the message belongs
        var msgEvent = msgActionEvent.Action; // message action added or removed
        var msgActionType = msgActionEvent.Event; // message action type
        var messageTimetoken = msgActionEvent.MessageTimetoken; // The timetoken of the original message
        var actionTimetoken = msgActionEvent.ActionTimetoken; //The timetoken of the message action
    },
    delegate(Pubnub pnObj, PNFileEventResult fileEvent)
    {
        //handle file message event
        var channelName = fileEvent.Channel;
        var chanelGroupName = fileEvent.Subscription;
        var fieldId = (fileEvent.File != null) ? fileEvent.File.Id : null;
        var fileName = (fileEvent.File != null) ? fileEvent.File.Name : null;
        var fileUrl = (fileEvent.File != null) ? fileEvent.File.Url : null;
        var fileMessage = fileEvent.Message;
        var filePublisher = fileEvent.Publisher;
        var filePubTT = fileEvent.Timetoken;
    },
    delegate(Pubnub pnObj, PNStatus pnStatus)
    {
        Console.WriteLine("{0} {1} {2}", pnStatus.Operation, pnStatus.Category, pnStatus.StatusCode);
        var affectedChannelGroups =
            pnStatus.AffectedChannelGroups; // The channel groups affected in the operation, of type array.
        var affectedChannels =
            pnStatus.AffectedChannels; // The channels affected in the operation, of type array.
        var category = pnStatus.Category; //Returns PNConnectedCategory
        var operation = pnStatus.Operation; //Returns PNSubscribeOperation
    }
));
        
//Add listener to receive Signal messages
SubscribeCallbackExt signalSubscribeCallback = new SubscribeCallbackExt(
    delegate (Pubnub pubnubObj, PNSignalResult<object> message) {
        // Handle new signal message stored in message.Message
    },
    delegate (Pubnub pubnubObj, PNStatus status)
    {
        // the status object returned is always related to subscribe but could contain
        // information about subscribe, heartbeat, or errors
    }
);
pubnub.AddListener(signalSubscribeCallback);
        
//Add listener to receive Events
SubscribeCallbackExt eventListener = new SubscribeCallbackExt(
    delegate (Pubnub pnObj, PNObjectEventResult objectEvent)
    {
        string channelMetadataId = objectEvent.Channel; // The channel
        string uuidMetadataId = objectEvent.UuidMetadata.Uuid; // The UUID
        string objEvent = objectEvent.Event; // The event name that occurred
        string eventType = objectEvent.Type; // The event type that occurred
        PNUuidMetadataResult uuidMetadata = objectEvent.UuidMetadata; // UuidMetadata
        PNChannelMetadataResult channelMetadata = objectEvent.ChannelMetadata; // ChannelMetadata
    },
    delegate (Pubnub pnObj, PNStatus status)
    {

    }
);
pubnub.AddListener(eventListener);
```

The `PNObjectEventResult` delegate is the one combined handler for this SDK. Its `Type` property, checked in the `if`/`else if` chain, is what tells a user, channel, or membership change apart.

### Go

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

go func() {
    for {
        select {
        case event := <-listener.UUIDEvent:
            fmt.Printf("User metadata event: %+v\n", event)
        case event := <-listener.ChannelEvent:
            fmt.Printf("Channel metadata event: %+v\n", event)
        case event := <-listener.MembershipEvent:
            fmt.Printf("Membership event: %+v\n", event)
        }
    }
}()

pn.AddListener(listener)
pn.Subscribe().Channels([]string{"channel_1"}).Execute()
```

`UUIDEvent`, `ChannelEvent`, and `MembershipEvent` are three separate channels on the same `Listener`, Go's version of three separate handlers.

### Rust

```rust
tokio::spawn(subscription.stream().for_each(|event| async move {
    match event {
        Update::AppContext(object) => {
            println!("App Context event: {:?}", object)
        }
        _ => {}
    }
}));
```

`Update::AppContext` is one case of the same tagged enum every event type arrives as on this combined stream.

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

```c
static void subloop_callback(pubnub_t *pbp, char const *message, enum pubnub_res result)
{
    if (PNR_OK == result) {
        /* App Context events arrive on this same callback as any other
           message. Check the parsed message's "type" and "event" fields
           to tell one apart from a regular published message. */
        printf("Message received: %s\n", message);
    }
}

pubnub_subloop_t *loop = pubnub_subloop_define(pubnub, "channel_1", pubnub_subscribe_defopts(), subloop_callback);
pubnub_subloop_start(loop);
```

C-Core (legacy) has no dedicated App Context handler. Every subscribe result, including an App Context event, arrives on this one generic callback.

### Swift

```swift
// Add a listener to receive App Context events
subscription.onAppContext = { appContextEvent in
  switch appContextEvent {
  case let .userMetadataSet(changeset):
    print("User metadata changes detected for \(changeset.metadataId) at \(changeset.updated).")
    print("All changes made to the object: \(changeset.changes)")
    print("To apply these changes, fetch the relevant object and call `changeset.apply(to: otherChannelMetadata)`.")
  case let .userMetadataRemoved(metadataId):
    print("Metadata for UUID \(metadataId) removed")
  case let .channelMetadataSet(changeset):
    print("Channel metadata changes detected for \(changeset.metadataId) at \(changeset.updated).")
    print("All changes made to the object: \(changeset.changes)")
    print("To apply these changes, fetch the relevant object and call `changeset.apply(to: otherUserMetadata)`.")
  case let .channelMetadataRemoved(metadataId: metadataId):
    print("Metadata for channel \(metadataId) removed")
  case let .membershipMetadataSet(membership):
    print("Membership set between \(membership.userMetadataId) and \(membership.channelMetadataId)")
  case let .membershipMetadataRemoved(membership):
    print("Membership removed between \(membership.userMetadataId) and \(membership.channelMetadataId)")
  }
}
```

`subscription.onAppContext` is the one combined handler, and the `switch` over its six cases is how this SDK spells `type` plus `event` together.

### Objective-C

```objectivec
@interface MyListener : NSObject <PNEventsListener>
@end

@implementation MyListener

- (void)client:(PubNub *)client didReceiveObjectEvent:(PNObjectEventResult *)event {
    if (event.data.uuidMetadata) {
        NSLog(@"User metadata event: %@", event.data.event);
    } else if (event.data.channelMetadata) {
        NSLog(@"Channel metadata event: %@", event.data.event);
    } else if (event.data.membership) {
        NSLog(@"Membership event: %@", event.data.event);
    }
}

@end

MyListener *listener = [MyListener new];
[pubnub addListener:listener];
[pubnub subscribeToChannels:@[@"channel_1"] withPresence:NO];
```

`didReceiveObjectEvent:` is the one combined handler. Its result carries `uuidMetadata`, `channelMetadata`, or `membership`, whichever one applies to this change.

### Dart

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

subscription.messages.listen((envelope) {
  // App Context events arrive on this same stream as any other message.
  // Check envelope.messageType or the payload itself to tell one apart
  // from a regular published message.
  print('Received: ${envelope.payload}');
});
```

Dart has no separate App Context stream. Everything, including an App Context event, arrives through this one generic `messages` stream.

### PHP

```php
class MyListener extends SubscribeCallback
{
    public function message($pubnub, $message)
    {
        $payload = $message->getMessage();

        // App Context events arrive on this same callback as any other
        // message. Check $payload['type'] and $payload['event'] to tell
        // one apart from a regular published message.
        echo 'Received: ' . json_encode($payload) . PHP_EOL;
    }

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

$pubnub->addListener(new MyListener());
$pubnub->subscribe()->channels('channel_1')->execute();
```

PHP's `SubscribeCallback` has no method dedicated to App Context events. They arrive on the same `message()` method as any other published message.

### Ruby

```ruby
callback = Pubnub::SubscribeCallback.new(
  message: ->(envelope) { puts "Message received: #{envelope.result[:data][:message]}" },
  object: ->(envelope) { puts "App Context event: #{envelope.result[:data]}" }
)

pubnub.add_listener(callback: callback)
pubnub.subscribe(channels: ['channel_1'])
```

The `object:` callback is Ruby's one combined handler, receiving every entity type on the same key.

### Unity

```csharp
// Adding listener.
pubnub.AddListener(new SubscribeCallbackListener(
    delegate(Pubnub pnObj, PNMessageResult<object> pubMsg)
    {
        Debug.Log(pubnub.JsonPluggableLibrary.SerializeToJsonString(pubMsg));
        var channelName = pubMsg.Channel;
        var channelGroupName = pubMsg.Subscription;
        var pubTT = pubMsg.Timetoken;
        var msg = pubMsg.Message;
        var publisher = pubMsg.Publisher;
    },
    delegate(Pubnub pnObj, PNPresenceEventResult presenceEvnt)
    {
        Debug.Log(pubnub.JsonPluggableLibrary.SerializeToJsonString(presenceEvnt));
        var action = presenceEvnt.Event; // Can be join, leave, state-change or timeout
        var channelName = presenceEvnt.Channel; // The channel for which the message belongs
        var occupancy = presenceEvnt.Occupancy; // No. of users connected with the channel
        var state = presenceEvnt.State; // User State
        var channelGroupName =
            presenceEvnt.Subscription; //  The channel group or wildcard subscription match (if exists)
        var publishTime = presenceEvnt.Timestamp; // Publish timetoken
        var timetoken = presenceEvnt.Timetoken; // Current timetoken
        var uuid = presenceEvnt.Uuid; // UUIDs of users who are connected with the channel
    },
    delegate(Pubnub pnObj, PNSignalResult<object> signalMsg)
    {
        Debug.Log(pubnub.JsonPluggableLibrary.SerializeToJsonString(signalMsg));
        var channelName = signalMsg.Channel; // The channel for which the signal belongs
        var channelGroupName =
            signalMsg.Subscription; // The channel group or wildcard subscription match (if exists)
        var pubTT = signalMsg.Timetoken; // Publish timetoken
        var msg = signalMsg.Message; // The Payload
        var publisher = signalMsg.Publisher; //The Publisher
    },
    delegate(Pubnub pnObj, PNObjectEventResult objectEventObj)
    {
        var channelName = objectEventObj.Channel; // Channel
        var channelMetadata = objectEventObj.ChannelMetadata; //Channel Metadata
        var uidMetadata = objectEventObj.UuidMetadata; // UUID metadata
        var evnt = objectEventObj.Event; // Event
        var type = objectEventObj.Type; // Event type
        if (objectEventObj.Type == "uuid")
        {
            /* got uuid metadata related event. */
        }
        else if (objectEventObj.Type == "channel")
        {
            /* got channel metadata related event. */
        }
        else if (objectEventObj.Type == "membership")
        {
            /* got membership related event. */
        }

        Debug.Log(pubnub.JsonPluggableLibrary.SerializeToJsonString(objectEventObj));
    },
    delegate(Pubnub pnObj, PNMessageActionEventResult msgActionEvent)
    {
        //handle message action
        var channelName = msgActionEvent.Channel; // The channel for which the message belongs
        var msgEvent = msgActionEvent.Action; // message action added or removed
        var msgActionType = msgActionEvent.Event; // message action type
        var messageTimetoken = msgActionEvent.MessageTimetoken; // The timetoken of the original message
        var actionTimetoken = msgActionEvent.ActionTimetoken; //The timetoken of the message action
    },
    delegate(Pubnub pnObj, PNFileEventResult fileEvent)
    {
        //handle file message event
        var channelName = fileEvent.Channel;
        var chanelGroupName = fileEvent.Subscription;
        var fieldId = (fileEvent.File != null) ? fileEvent.File.Id : null;
        var fileName = (fileEvent.File != null) ? fileEvent.File.Name : null;
        var fileUrl = (fileEvent.File != null) ? fileEvent.File.Url : null;
        var fileMessage = fileEvent.Message;
        var filePublisher = fileEvent.Publisher;
        var filePubTT = fileEvent.Timetoken;
    },
    delegate(Pubnub pnObj, PNStatus pnStatus)
    {
        Debug.Log($"{pnStatus.Operation} {pnStatus.Category} {pnStatus.StatusCode}");
        var affectedChannelGroups =
            pnStatus.AffectedChannelGroups; // The channel groups affected in the operation, of type array.
        var affectedChannels =
            pnStatus.AffectedChannels; // The channels affected in the operation, of type array.
        var category = pnStatus.Category; //Returns PNConnectedCategory
        var operation = pnStatus.Operation; //Returns PNSubscribeOperation
    }
));

//Add listener to receive Signal messages
SubscribeCallbackListener signalSubscribeCallback = new SubscribeCallbackListener();
signalSubscribeCallback.onSignal += delegate (Pubnub pubnubObj, PNSignalResult<object> message) {
  // Handle new signal message stored in message.Message
 };
signalSubscribeCallback.onStatus += delegate (Pubnub pubnubObj, PNStatus status) {
  // the status object returned is always related to subscribe but could contain
  // information about subscribe, heartbeat, or errors
 };
pubnub.AddListener(signalSubscribeCallback);

//Add listener to receive Events
SubscribeCallbackListener eventListener = new SubscribeCallbackListener();
eventListener.onObject += delegate(Pubnub pnObj, PNObjectEventResult objectEvent) {
 string channelMetadataId = objectEvent.Channel; // The channel
 string uuidMetadataId = objectEvent.UuidMetadata.Uuid; // The UUID
 string objEvent = objectEvent.Event; // The event name that occurred
 string eventType = objectEvent.Type; // The event type that occurred
 PNUuidMetadataResult uuidMetadata = objectEvent.UuidMetadata; // UuidMetadata
 PNChannelMetadataResult channelMetadata = objectEvent.ChannelMetadata; // ChannelMetadata
};
pubnub.AddListener(eventListener);
```

Unity shares its underlying API with C#, so its `PNObjectEventResult` delegate is the same one-combined-handler shape, discriminated the same way by `Type`.

### Unreal Engine

```cpp
// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::ChannelEntityAllListenersSample()
{
	
	//Assumes PubnubClient is created and UserID is set

	// Create a channel entity
	FString ChannelName = TEXT("comprehensive_channel");
	UPubnubChannelEntity* ChannelEntity = PubnubClient->CreateChannelEntity(ChannelName);

	// Create a subscription with presence events enabled to receive all event types
	FPubnubSubscribeSettings SubscriptionSettings;
	SubscriptionSettings.ReceivePresenceEvents = true;
	UPubnubSubscription* Subscription = ChannelEntity->CreateSubscription(SubscriptionSettings);

	// Add ALL listener types to handle different PubNub events

	// 1. Message Listener - Fires when regular messages are published to the channel
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->OnPubnubMessage.AddDynamic(this, &ASample_ChannelEntity::OnMessage_AllListenersSample);

	// 2. Signal Listener - Fires when signals are sent to the channel
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->OnPubnubSignal.AddDynamic(this, &ASample_ChannelEntity::OnSignal_AllListenersSample);

	// 3. Presence Event Listener - Fires when users join/leave/timeout on the channel
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->OnPubnubPresenceEvent.AddDynamic(this, &ASample_ChannelEntity::OnPresenceEvent_AllListenersSample);

	// 4. Object Event Listener - Fires when App Context metadata changes occur
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->OnPubnubObjectEvent.AddDynamic(this, &ASample_ChannelEntity::OnObjectEvent_AllListenersSample);

	// 5. Message Action Listener - Fires when message actions/reactions are added or removed
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->OnPubnubMessageAction.AddDynamic(this, &ASample_ChannelEntity::OnMessageAction_AllListenersSample);

	// 6. Universal Listener - Fires for ANY type of PubNub event (catch-all)
	// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
	Subscription->FOnPubnubAnyMessageType.AddDynamic(this, &ASample_ChannelEntity::OnAnyEvent_AllListenersSample);

	// Subscribe to start receiving all event types
	Subscription->SubscribeAsync();
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnMessage_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("MESSAGE LISTENER - Content: %s, Channel: %s, User: %s"), 
		*Message.Message, *Message.Channel, *Message.UserID);
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnSignal_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("SIGNAL LISTENER - Content: %s, Channel: %s, User: %s"), 
		*Message.Message, *Message.Channel, *Message.UserID);
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnPresenceEvent_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("PRESENCE LISTENER - Event: %s, Channel: %s"), 
		*Message.Message, *Message.Channel);
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnObjectEvent_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("OBJECT EVENT LISTENER - Event: %s, Channel: %s"), 
		*Message.Message, *Message.Channel);
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnMessageAction_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("MESSAGE ACTION LISTENER - Action: %s, Channel: %s"), 
		*Message.Message, *Message.Channel);
}

// ACTION REQUIRED: Replace ASample_ChannelEntity with name of your Actor class
void ASample_ChannelEntity::OnAnyEvent_AllListenersSample(FPubnubMessageData Message)
{
	UE_LOG(LogTemp, Log, TEXT("UNIVERSAL LISTENER - Type: %d, Content: %s, Channel: %s"), 
		(int32)Message.MessageType, *Message.Message, *Message.Channel);
}
```

`Subscription->OnPubnubObjectEvent` is the one combined delegate for App Context events, alongside the other event delegates on the same entity subscription.

Check your SDK's API reference in [Available SDKs](https://www.pubnub.com/docs/sdks.md) to confirm which pattern it follows and the exact fields its result carries.

## Client-side and server-side events observe the same change independently

The same set or delete that produces an `onObjects` event also generates a server-side event. [Events & Actions](https://www.pubnub.com/docs/integrations/event-forwarding/overview.md) can act on that event, for example by triggering a webhook or writing to a queue. These are two separate consumption paths for one underlying change, not one feeding the other. A client-side subscriber sees the event only if it's subscribed to the right channel and the keyset toggle for that entity is on. Events & Actions observes the platform directly instead, so it needs no subscription.

Events & Actions' CRUD event types name the same three entities, but split each into three distinct types instead of App Context's `set` and `delete`:

| Source | Event type | Fires when |
| --- | --- | --- |
| Users | User created | User metadata is set for the first time |
| Users | User updated | Existing user metadata changes |
| Users | User deleted | User metadata is removed |
| Channels | Channel created | Channel metadata is set for the first time |
| Channels | Channel updated | Existing channel metadata changes |
| Channels | Channel deleted | Channel metadata is removed |
| Memberships | Membership created | A membership record is set for the first time |
| Memberships | Membership updated | An existing membership record changes |
| Memberships | Membership deleted | A membership record is removed |

The Users and Channels sources in Events & Actions also carry unrelated Presence-driven event types, such as a user's subscription starting or stopping. Only the CRUD-produced types listed above correspond to an App Context change. For the full event and action list, refer to [Event / Action List](https://www.pubnub.com/docs/integrations/event-forwarding/event-sources.md).

## Next steps

* [App Context](https://www.pubnub.com/docs/data-storage/metadata/overview.md). The three entity types, how to turn on App Context, and where each event type is delivered.
* [Event listeners](https://www.pubnub.com/docs/pub-sub/subscribe/event-listeners.md). How a handler is registered and what its scope is.
* [Events](https://www.pubnub.com/docs/architecture/events.md#app-context). How App Context events fit among PubNub's other client-side and server-side event types.
* [App Context filtering](https://www.pubnub.com/docs/data-storage/metadata/filtering.md). Query App Context data instead of paging through everything.
* [Events & Actions](https://www.pubnub.com/docs/integrations/event-forwarding/overview.md). Configure a webhook, queue, or stream from a server-side event.
* [Core concepts](https://www.pubnub.com/docs/architecture/core-concepts.md). User IDs, channels, and memberships.

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