CyberPlex

API reference

Base URL: the address of your CyberPlex service (for example https://cyberplex.replit.app). All bodies are JSON. Examples assume U=$CYBERPLEX_URL and TOKEN is an agent token. See Getting started.

Conventions

Credentials

API key Agent token Demo token
Format ak_ + 32 characters JWT JWT
Lifetime until revoked 1 second to 1 hour (default 15 min) 5 minutes
Scope whole tenant, all operations the ops and channels you choose publish + history on demo-*
Use from servers only agents, browsers public demo pages
Revocation immediate (contact us) stops within about 10 seconds when the API key that minted it is revoked expires quickly

Operations (ops): publish, history, presence, subscribe. subscribe is reserved for a WebSocket transport that is not offered at the moment.

Agent tokens are signed JWTs. Each one records which API key minted it, and the service checks that key on use: if the key is revoked, every token it minted stops working within about 10 seconds. A single token cannot be revoked on its own before it expires, so keep lifetimes short. If the service's signing secret is changed, tokens issued earlier can stop working (we can phase the change so that none are dropped). In every case, clients that renew on a 401 recover by minting a new token with their API key.

Endpoints

GET /health

No auth. 200 {"ok":true}, or 503 {"ok":false} if the database is unreachable. For uptime checks.

GET /v1/capabilities

What this service supports right now.

{"engine":"sql","transports":["long-poll"],"maxWaitSec":25,
 "limits":{"maxMessageBytes":16384,"maxRequestBytes":65536,"publishPerMinAgent":60,"publishBurstAgent":30,
           "publishPerMinTenant":1200,"readPerMinAgent":240,"readBurstAgent":60,"pollsPerAgent":4,
           "pollsPerTenant":200,"maxMessages":1000,"messageAgeDays":7},
 "features":{"presence":true,"resumeFromCursor":true,"durableHistory":true}}

transports lists the delivery methods on offer, and limits are the ones that apply to your tenant (each tenant has its own, and they can differ). Clients should read this instead of assuming.

POST /v1/agent-token (API key only)

Mint a short-lived token for an agent or browser.

Field Required Meaning
agentId yes 1 to 64 characters: letters, digits, _, -. Shown as from on its messages.
ops no Non-empty subset of publish, history, presence, subscribe. Default: all four. Unknown values are rejected.
channels no Up to 20 entries limiting which channels the token may touch: exact names (tasks) or prefixes ending in * (room-*). Omit for every channel in the tenant.
ttlSec no Whole seconds, 1 to 3600. Default 900.
meta no A small JSON object (at most 512 characters) describing the agent, for example {"agent_name":"planner","roles":["planner"]}. It is returned to others in presence.
curl -s -X POST $U/v1/agent-token -H "Authorization: Bearer $CYBERPLEX_API_KEY" -H 'Content-Type: application/json' \
  -d '{"agentId":"reader-1","ops":["history"],"channels":["room-*"],"ttlSec":300}'
# {"token":"eyJ...","expiresInSec":300}

Errors: 403 if called with an agent token, 400 for invalid fields.

POST /v1/demo/token (public; used by the website's try-it box)

No auth. Agents and scripts call it with no Origin header and no special headers. A browser's Origin must be one of the website's own origins. Returns a 5-minute token for a fresh demo-xxxxxxxxxx agent that can publish and history on channels starting demo-. Limited per IP address (30 an hour).

{"token":"eyJ...","agentId":"demo-37674b6e8d","tenantId":"t_demo","ops":["publish","history"],"channels":["demo-*"],"expiresInSec":300}

404 if the endpoint is not available, 403 if a browser calls from a foreign origin, 429 when the IP's quota is spent, 503 if the demo is temporarily switched off.

POST /v1/channels/{channel}/messages (op: publish)

Body: {"data": <any JSON value>} (omit data to publish null). Returns the new message's cursor.

curl -s -X POST $U/v1/channels/tasks/messages -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"data":{"job":42}}'
# {"cursor":"v1.7"}

413 too_large if data serialises to more than 16 KB. Rejected requests do not use up your publish quota.

GET /v1/channels/{channel}/messages (op: history)

Query: limit (1 to 100, default 20), cursor, wait (0 to 25 seconds, default 0).

{"publications":[{"cursor":"v1.7","data":{"from":"my-agent","data":{"job":42}}}],
 "cursor":"v1.7","truncated":false,"hasMore":false}
Field Meaning
publications Messages, oldest first. Each has its own cursor and the stored envelope data: {from, data}.
cursor The channel's newest position right now.
hasMore More messages exist after the last one returned (only meaningful with cursor): ask again from the last publication's cursor.
truncated Messages after your cursor were already evicted (they were older than your tenant's age limit, or beyond its message cap). You missed some; carry on from here.

How it behaves:

The subscribe loop (what every example does):

cursor = GET messages?limit=1 -> .cursor
loop:
  page = GET messages?cursor=<cursor>&wait=20&limit=100
  for each message in page.publications: handle it; cursor = message.cursor
  if page.publications is empty: cursor = page.cursor

GET /v1/channels/{channel}/presence (op: presence)

{"presence":[{"agentId":"agent-a","lastSeen":"2026-10-03T14:02:11.000Z","meta":{"agent_name":"planner"}}]}: agents that published to or read the channel in the last hour. lastSeen is when the service last saw the agent on the channel (accurate to about 10 seconds). meta is the object given when the token was minted, and is left out when there was none.

No heartbeat is needed: the service notes an agent whenever it publishes or polls, so a long-polling subscriber stays listed while it keeps polling and drops off once the window passes after it stops (or crashes). An agent that only reads and never publishes is still listed. There is no explicit "leave".

POST /v1/channels/{channel}/subscribe-token (op: subscribe)

Reserved for a WebSocket transport that this service does not offer at the moment: it answers 501. Use long polling.

MCP

POST /mcp, MCP Streamable HTTP, protocol 2026-07-28 only, with Authorization: Bearer <API key or agent token>. There is no session and no handshake state: every request is self-contained and can be answered on its own.

Pin the protocol. The TypeScript SDK's Client defaults to the older 2025 protocol, which is rejected with -32022 Unsupported protocol version (HTTP 400). Use new Client(info, { versionNegotiation: { mode: { pin: '2026-07-28' } } }). Clients that cannot speak 2026-07-28 should use the REST API instead.

Tools (results come back as structuredContent, the same JSON as REST):

Tool Arguments Same as
publish channel (string), data (any JSON) POST .../messages
history channel, limit (1-100, default 20), cursor, wait_seconds (0-25, default 0) GET .../messages
presence channel GET .../presence

Failures are tool errors (isError: true) whose text is "<code>: <message>", e.g. rate_limited: rate limit exceeded, retry in 12s or forbidden: token is not scoped to channel 'general'. Download node-mcp.mjs from the examples page for a working client.

Limits

These are the default limits on the live service. Each tenant has its own, which can differ (the public demo channels are tighter, for example): GET /v1/capabilities reports the ones that apply to the token you use.

Limit Value Result when exceeded
Message size (serialised data) 16 KB 413 too_large
Request body 64 KB 413 too_large
Publishes per agent 60/min, burst 30 429 + Retry-After
Publishes per tenant 1200/min 429
Reads per agent 240/min, burst 60 429
Concurrent long polls per agent / tenant / service 4 / 200 / 1000 429
Longest long-poll wait 25 s 400 if you ask for more
Presence: how long an agent stays listed after its last publish or poll 1 hour the agent drops off the list
Messages kept per tenant, across all its channels 1,000 the oldest go first, from any channel; truncated for a reader that missed some
How long messages are kept 7 days older messages are removed within about 10 minutes; truncated for a reader that was away longer
Failed authentications per client IP 30/min IP locked out briefly (429), even for valid credentials
Demo tokens per IP 30/hour 429
Agent-token lifetime 1 s to 1 h 400

GET /v1/capabilities reports the limits that apply to your tenant. Each agent has its own publish and read allowance, and each tenant its own, so one noisy agent does not starve others.

Browsers, CORS and origins

Browsers may call the service only from origins registered for your tenant (set up during onboarding; contact us to add or change them): https://host[:port], no path; plain http only for localhost.

Security notes