Skip to content
Cohort 10 · Closed Request a seat →

Documentation

topcmesh API

Two surfaces: an HTTP publish API and a WebSocket subscribe protocol. Everything else in this document is a detail of one of those two.

Examples use namespace q2m9r1 and the host bus.topcmesh.com. Substitute your own namespace — it is printed on your mesh key card. Publishing or subscribing to a namespace you do not hold a token for returns 403 namespace_not_authorized.

Don't have a namespace yet? Request a cohort seat →

01

Base URL and versioning

Every request goes to https://bus.topcmesh.com. The mesh is versioned by header, not by path — a path version would have forced a migration on people who only wanted a bug fix.

Send Topcmesh-Version: 2026-04-01 on publish requests and as a query parameter on the WebSocket handshake. Omit it and you are pinned to the version that was current when your namespace was created, which never changes underneath you.

MethodRoutePurpose
POST/bus/namespace/{ns}/topic/{topic}Publish a message to a topic
GET/bus/namespace/{ns}/topic/{topic}Topic metadata — subscriber count, cursor head, retention
GET/bus/namespace/{ns}/topicsList topics in the namespace
WSS/bus/namespace/{ns}/subscribeOpen a subscriber connection
POST/bus/namespace/{ns}/tokensMint a scoped, short-lived subscriber token

A topic name is a string of up to 128 characters from [a-z0-9._-]. Dots carry no meaning to the mesh — orders.status is one flat name, not a hierarchy, and there is no wildcard subscription. This is deliberate; see the capability list for why the topic space stays flat.


02

Authentication

There are two token classes and they are not interchangeable.

  • Publish tokens are long-lived, server-side only, and scoped to a namespace with an optional topic prefix. Never ship one to a browser — a publish token can write to every topic in its scope.
  • Subscriber tokens are short-lived (300 seconds by default, 3,600 maximum), minted by your backend, and scoped to an explicit list of topics. These are the ones that reach clients.
mint-token.js
// Runs on your server. The publish token never leaves it.
const res = await fetch(
  'https://bus.topcmesh.com/bus/namespace/q2m9r1/tokens', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.TOPCMESH_PUBLISH_TOKEN}`,
    'Content-Type': 'application/json',
    'Topcmesh-Version': '2026-04-01',
  },
  body: JSON.stringify({ topics: ['orders.status'], ttl: 300 }),
});

// 201 Created
// { "token": "tms_sub_9f2a…", "expires_at": "2026-07-26T14:07:11Z",
//   "topics": ["orders.status"], "namespace": "q2m9r1" }

Both token classes carry the same claim set. The mesh checks ns against the route on every request and topics on every attach.

ClaimMeaning
nsNamespace ID. Must match the namespace in the route or the request is rejected.
topicsExplicit topic list for subscriber tokens; a prefix pattern for publish tokens.
expExpiry, Unix seconds. A token that expires mid-connection closes it with 4401.
kidSigning key ID. Rotate signing keys without invalidating live tokens.

03

Publishing

One POST per message. The mesh accepts the message, assigns it a cursor, and fans it out to every connection currently attached to the topic. Publishing is synchronous with respect to acceptance and asynchronous with respect to delivery.

publish.sh
curl -X POST https://bus.topcmesh.com/bus/namespace/q2m9r1/topic/orders.status \
  -H "Authorization: Bearer $TOPCMESH_PUBLISH_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Topcmesh-Version: 2026-04-01" \
  -d '{"order_id":"ord_8f2a","state":"picked","at":"2026-07-26T14:02:11Z"}'
202 Accepted
{
  "cursor": "84f21c",
  "topic": "orders.status",
  "namespace": "q2m9r1",
  "fanout": 1284,
  "accepted_at": "2026-07-26T14:02:11.442Z"
}

fanout is the number of connections the message was queued to at acceptance time. It is not a delivery confirmation — a connection can drop between acceptance and write. Per-subscriber delivery is observable in the namespace log, keyed by connection ID.

Bodies may be any content type up to 256 KB. The mesh does not inspect, transform, or re-encode the payload; Content-Type is passed through to subscribers verbatim. If you need larger objects, publish a reference and let the client fetch it.


04

Subscribing

Open one WebSocket per client and attach to as many topics as the token allows. There are four frame types and that is the entire protocol.

FrameDirectionPurpose
attachclient → meshAttach to a topic, optionally presenting a cursor
messagemesh → clientA published message with its cursor and topic
replaymesh → clientMarks the start of a replayed gap: {from, count}
ackclient → meshAcknowledge a cursor. Optional — the SDK does it for you
subscribe.js
import { connect } from '@topcmesh/client';      // 9 KB gzipped

const mesh = connect({
  namespace: 'q2m9r1',
  token: await mintSubscriberToken(),        // from your backend
  cursor: localStorage.getItem('cursor') ?? 'latest',
});

mesh.topic('orders.status').on('message', (msg) => {
  render(msg.data);
  localStorage.setItem('cursor', msg.cursor);   // acknowledge
});

mesh.on('replay', ({ from, count }) =>
  console.log(`replayed ${count} from ${from}`));  // after a reconnect

If you would rather not take the dependency, the raw frames are plain JSON and the whole exchange looks like this:

frames.txt
→ wss://bus.topcmesh.com/bus/namespace/q2m9r1/subscribe?v=2026-04-01
  Sec-WebSocket-Protocol: topcmesh.v1, tms_sub_9f2a…

→ client sends
{"type":"attach","topic":"orders.status","cursor":"84f1a0"}

← mesh sends (gap first, then live — contiguous by construction)
{"type":"replay","topic":"orders.status","from":"84f1a0","count":12}
{"type":"message","cursor":"84f1a1","data":{…}}
{"type":"message","cursor":"84f1a2","data":{…}}

→ client acknowledges (optional, batched is fine)
{"type":"ack","topic":"orders.status","cursor":"84f1a2"}

05

Cursors and replay

A cursor is an opaque, monotonically increasing six-character token scoped to a single topic. It is not a timestamp, not an offset, and not comparable across topics. Do not parse it — the encoding has changed once already and will change again.

Attach with one of three values:

  • 'latest' — live only. Nothing before the moment of attach is delivered.
  • '<cursor>' — resume. Everything after that cursor is replayed in order, then live delivery continues.
  • 'earliest' — start from the retention floor, which is the oldest message still inside the topic's window.

On a resume the mesh emits a replay frame carrying from and count, then the gap in order, then live messages. There is no end-of-replay marker, because there is no boundary: the replayed messages and the live ones are a single ordered run, and a client that treats them identically is correct.

A cursor older than the topic's retention window returns 409 cursor_expired. This is not an outage and not a bug — it is the documented consequence of a finite buffer. The correct response is to resynchronize from your own source of truth and re-attach at 'latest'. If you find yourself hitting it routinely, the topic's retention window is set too short for how long your clients stay away.

TierDefault retentionMaximumConfigurable per topic
Signal60 s60 sNo
Mesh1 h6 hYes
Fabric6 h24 hYes

Retention is per topic, so a high-volume telemetry topic can hold sixty seconds while a low-volume order topic holds six hours in the same namespace. Buffers are sized by time, not by message count.


06

Backpressure

Every subscriber connection has an 8 MB outbound buffer. A client that stops reading — a throttled background tab, a device on a saturated link, a process that stalled in a render loop — fills that buffer and then gets disconnected with slow_consumer and WebSocket close code 4290.

The disconnection is deliberate and it is the safe outcome. The SDK reconnects with its last acknowledged cursor, which turns a slow consumer into a replay rather than into data loss — provided it recovers inside the retention window. The alternative designs are unbounded buffering, which turns one bad client into a memory incident, or silent dropping, which is data loss without a signal.

Each event is written to the namespace log with the connection ID, the topic, the buffered byte count and the timestamp. You get the specific connection, not an aggregate you then have to go correlate against your own logs.

namespace-log.jsonl
{"event":"slow_consumer","conn":"c_7a1f92","topic":"fleet.telemetry",
 "buffered_bytes":8388608,"last_ack":"2ad5e9","at":"2026-07-26T14:03:58.117Z"}

07

Limits

These are published because you should be able to model the mesh before you depend on it. Sustained operation above a tier ceiling is a conversation about tiers, not an immediate cutoff.

LimitSignalMeshFabric
Peak concurrent subscribers20025,000250,000+
Topics per namespace3unlimitedunlimited
Published messages / month100,00020Mnegotiated
Publish rate, per topic100 /s2,000 /s2,000 /s
Publish rate, per namespace200 /s10,000 /snegotiated
Max payload64 KB256 KB256 KB
Subscriber token TTL, max900 s3,600 s3,600 s
Outbound buffer per subscriber2 MB8 MB8 MB
Regions13dedicated

The mesh does not buffer publishes on your behalf. Exceeding a publish rate returns 429 publish_rate_exceeded with a Retry-After header in seconds; retrying is your call and your backoff.


08

Error codes

Errors return a machine-readable code, a human sentence, and nothing else. There is no error taxonomy to learn beyond this table.

CodeStatusWhat to do
namespace_not_authorized403The token's ns claim doesn't match the namespace in the route. Check which key you loaded.
topic_not_authorized403The subscriber token doesn't list this topic. Mint a new one that does.
cursor_expired409Cursor is older than the topic's retention window. Resynchronize from your own source of truth and re-attach at latest.
publish_rate_exceeded429Over the topic or namespace ceiling. Retry-After is in seconds; the mesh does not buffer publishes.
payload_too_large413Over the tier's payload ceiling. The mesh does not chunk — publish a reference instead.
subscriber_ceiling503Namespace is at its peak-subscriber ceiling. Existing connections are unaffected; new attaches are refused.
topic_not_found404Only on GET. Publishing to an unknown topic creates it.
slow_consumerWS 4290Outbound buffer exceeded. Reconnect with your last acknowledged cursor.
token_expiredWS 4401Subscriber token TTL elapsed mid-connection. Mint a fresh one and re-attach.
version_unsupported400Topcmesh-Version names a version that has been retired. Versions are supported for 24 months.

09

SDKs

Three clients, all thin. They handle the reconnect loop, cursor persistence and token refresh. Everything else you would write yourself in an afternoon, and section 04 is the whole protocol if you would rather.

PackageRuntimeSizeInstall
@topcmesh/clientBrowser, Node 18+, Deno, Bun9 KB gznpm i @topcmesh/client
topcmesh-pyPython 3.10+, asynciopip install topcmesh
topcmesh-goGo 1.21+go get go.topcmesh.com/mesh

No SDK has a dependency beyond its standard library. The browser client ships as ESM only and has no polyfills — if your target predates WebSocket and AbortController, the raw protocol is your path.