Documentation

Payload Formats

Encode text or JSON, validate received data, and adapt a serializer without changing the SDK.

Before you start: obtain a connected chat segment using Messaging. Each format below is an alternative: sender and receiver must agree on the format.

1. Start with text

from useceleris_client import read_text, text_payload

stop_text = chat.on_message(lambda payload, metadata: print(read_text(payload)))
await chat.publish(text_payload("Hello"))
# Later, when this receiver is no longer needed:
stop_text()

Celeris carries opaque bytes. It does not infer a content type, compress JSON, or translate formats for recipients. read_text raises ConfigurationError for bytes that are not valid UTF-8.

2. Send and validate JSON

read_json returns whatever the JSON holds. Validate anything from a publisher you do not control; Pydantic, which the client already depends on, is one way:

from typing import Literal

from pydantic import BaseModel, ValidationError

from useceleris_client import ConfigurationError, MessageMetadata, json_payload, read_json


class Typing(BaseModel):
    type: Literal["typing"]
    active: bool


def receive(payload: bytes, metadata: MessageMetadata) -> None:
    try:
        event = Typing.model_validate(read_json(payload))
    except (ConfigurationError, ValidationError):
        print("Ignored an invalid typing event.")
        return

    print("Peer typing:", event.active)


stop_json = chat.on_message(receive)
await chat.publish(json_payload({"type": "typing", "active": True}))

json_payload writes compact JSON, as JavaScript's JSON.stringify does. Floats keep Python's spelling (1.0, 1e-07 where JavaScript writes 1, 1e-7), so compare decoded values across SDKs, not bytes. It raises ConfigurationError for values JSON cannot represent: NaN, infinities, sets, bytes, other non-JSON types and circular structures. read_json raises ConfigurationError for invalid UTF-8 or invalid JSON. Render received text safely rather than as HTML.

3. Bring your own serializer

The SDK includes one adapter rather than a bundled library for every format. Your encoder must return bytes. Errors raised by your encoder or decoder propagate unchanged.

MessagePack

pip install msgpack
import msgpack

from useceleris_client import MessageMetadata, create_payload_codec

codec = create_payload_codec(
    encode=lambda value: msgpack.packb(value),
    decode=lambda payload: msgpack.unpackb(payload),
)


def receive(payload: bytes, metadata: MessageMetadata) -> None:
    reading = codec.read_payload(payload)
    print(reading)  # Validate the decoded value before trusting it.


stop_binary = chat.on_message(receive)
await chat.publish(codec.encode_payload({"sensor": "temperature", "value": 21.5}))

MessagePack keeps binary fields and distinguishes integers from floats, which JSON cannot.

Protobuf

With classes generated by protoc from a schema such as:

reading.proto
syntax = "proto3";

package demo;

message Reading {
  string sensor = 1;
  double value = 2;
}
from reading_pb2 import Reading  # generated by protoc --python_out

from useceleris_client import create_payload_codec

readings = create_payload_codec(
    encode=lambda reading: reading.SerializeToString(),
    decode=Reading.FromString,
)

await chat.publish(readings.encode_payload(Reading(sensor="temperature", value=21.5)))

The numbers in the schema identify fields on the wire; keep them stable as the schema evolves. A Python receiver and a JavaScript sender interoperate as long as both use the same schema.

Do not mix text, JSON, MessagePack and Protobuf receivers on one segment unless your application defines how to tell them apart; separate segments or an explicit envelope can. Successfully decoding a payload does not prove who sent it.

4. Handle timestamps

The SDK's metadata and your payload are separate: your serializer decides the types inside a payload. The SDK's own numbers are all int:

ValuePython type
Presence request page and per_pageint
Presence result total, per_page, current_page, from_, toint
Connection and presence-query timeouts, in millisecondsint
Credential request disconnected_at and replay_lookback_msint or None
Recovery event retry_indexint
Message, notice, presence-event and presence-record timestampint, Unix ms

A timestamp converts to a datetime directly, and serializes to JSON as a number with no loss:

import json
from datetime import datetime, timezone

from useceleris_client import MessageMetadata


def record(payload: bytes, metadata: MessageMetadata) -> None:
    sent_at = datetime.fromtimestamp(metadata.timestamp / 1000, tz=timezone.utc)
    print(sent_at.isoformat())
    print(json.dumps({"message_id": metadata.message_id, "timestamp": metadata.timestamp}))


stop_metadata = chat.on_message(record)

A JavaScript consumer of that JSON should read large timestamps as bigint if it needs their exact value. Do not log sensitive payloads, identities or error resources in production.

Next: Presence. For format tradeoffs, see Binary Messaging.

We use Google Analytics cookies to understand how people use Celeris, only if you allow it. See our Cookie Policy.