Publish a message to a channel

​

Publishes a message to a channel. The same path serves two distinct callers on two hosts — the host you target determines how you authenticate and what the body contains:

  • Control plane (api.useportal.co) — server publish. Authenticate with a secret key and include senderId in the body (the server names the sender; there is no user JWT to derive it from). A server publish is pre-trusted: it bypasses your channel config's publish middleware and authorization, so it always lands. Platform limits still apply (content ≤ 2 KB; per-channel rate limit). The notification bridge that your config can wire to notify does not run for a server publish — use /v1/users/{userId}/notifications to deliver notifications.
  • Realtime edge (realtime.useportal.co) — client publish. Authenticate with a user JWT and omit senderId (the sender is the token's subject). A client publish is subject to your channel config's authorization and publish middleware, which may block, mask, or defer it.

content is opaque to the platform and capped at 2 KB. kind defaults to text (the only value in v1). type is a userland discriminator (defaults to message). Set to to a member's user id to deliver to that one member only (no broadcast) and write them an inbox item. mentions is a list of { userId } declared by the sender; the platform verifies each is a member, dedupes, and caps the count.

Path Parameters

  • The channel id. Channels are not created explicitly — they exist by being used.

Body

required
application/json
  • Use ServerPublishRequest on the control plane (with a secret key) and ClientPublishRequest on the realtime edge (with a user JWT).

    Server publish body — used on the control plane with a secret key. Pre-trusted: bypasses your config's publish middleware and authorization.

    • The message payload. Opaque to the platform; ≤ 2 KB.

    • The sender's user id. Required on the server path (there is no user JWT to derive it from).

    • Content class. Defaults to text (the only value in v1).

    • Declared mentions. The platform verifies each is a member, dedupes, and caps the count.

      Properties: 1
    • Reply into a thread: the id of the message that started it — that id IS the thread id. The reply is stored in the channel and delivered live to every channel connection like any other message; what narrows is notification, which goes only to the thread's participants (users who posted in or were mentioned in it).

      The parent is not required to still be resident in the channel's hot storage. Mutually exclusive with to — sending both is rejected validation_failed.

      Threads nest, to a cap of 8 levels: a reply whose thread would sit deeper is rejected validation_failed with reason: thread_depth_exceeded. A threadParentId naming a message the channel cannot have minted yet is rejected validation_failed with reason: thread_parent_unknown — an id whose message has merely aged out is still accepted, and its thread is recorded as top-level when the ancestry can no longer be resolved.

    • A member's user id. Set this to deliver to that member only (no broadcast) and write them an inbox item.

    • Userland discriminator. Defaults to message.

Responses

  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
  • application/json
Request Example for post/v1/channels/{channelId}/messages
curl https://api.useportal.co/v1/channels/chat-general/messages \
  --request POST \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
  --data '{
  "senderId": "server",
  "type": "system",
  "content": {
    "text": "Welcome to the channel!"
  },
  "kind": "text"
}'
Platform-assigned id, per-channel sequence, and timestamp
{
  "id": "m_1752912000_42",
  "seq": 42,
  "timestamp": 1752912000000
}