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 includesenderIdin 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 tonotifydoes not run for a server publish — use/v1/users/{userId}/notificationsto deliver notifications. -
Realtime edge (
realtime.useportal.co) — client publish. Authenticate with a user JWT and omitsenderId(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
- Type: stringchannel
Id requiredThe channel id. Channels are not created explicitly — they exist by being used.
Body
Use
ServerPublishRequeston the control plane (with a secret key) andClientPublishRequeston 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.
- contentrequired
The message payload. Opaque to the platform; ≤ 2 KB.
- Type: stringsender
Id requiredThe sender's user id. Required on the server path (there is no user JWT to derive it from).
- Type: stringkind
Content class. Defaults to
text(the only value in v1). - Type:mentionsProperties: 1
Declared mentions. The platform verifies each is a member, dedupes, and caps the count.
- Type: stringthread
Parent Id max length:128Reply 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 rejectedvalidation_failed.Threads nest, to a cap of 8 levels: a reply whose thread would sit deeper is rejected
validation_failedwithreason: thread_depth_exceeded. AthreadParentIdnaming a message the channel cannot have minted yet is rejectedvalidation_failedwithreason: 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. - Type: stringto
A member's user id. Set this to deliver to that member only (no broadcast) and write them an inbox item.
- Type: stringtype
Userland discriminator. Defaults to
message.
Responses
- application/json
- application/json
- application/json
- application/json
- application/json
- application/json
- application/json
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"
}'
{
"id": "m_1752912000_42",
"seq": 42,
"timestamp": 1752912000000
}