Skip to content

The public HTTP routes

Call the Realtime HTTP API with the correct credentials, scopes, and request shapes.

Use these five HTTP routes from your backend to issue end-user tokens, publish messages, read history and read presence. Every route requires a product public key; the SDK is optional.

Namespace, grant, webhook and archive management use separate management surfaces. The workspace provides Realtime → Namespaces, Grants, Webhooks and Archive; the server SDK covers its management methods. Manage product public keys in Product → Settings → Public Access.

Base path

https://public.lepsto.com/{productId}/realtime

Replace {productId} with your product’s ID. Paths below are relative to this base; for example, publish to POST https://public.lepsto.com/{productId}/realtime/messages. Payload examples use one chat application: replace chat:room-1 and user-8412 with your channel and authenticated user.

Authentication

Authenticate each request with your backend’s product public key:

Authorization: Bearer lpk_…

New keys from Product → Settings → Public Access contain lpk_ followed by 32 hexadecimal characters. Existing imported rtk_… keys remain valid with their original values.

The legacy X-Api-Key header is still accepted. Authorization takes precedence if both are sent; use it for new integrations.

The edge checks the key before forwarding the request:

Status
code
When
401
public_key_required
No key was presented
401
public_key_invalid
The key matches no active key of the product in the path — unknown, mistyped, revoked (once the revocation has propagated), or belonging to another product
403
public_key_scope
The key is active but its scope does not cover this route
503
public_config_unavailable
No key snapshot is loaded and the edge fails closed. Retry with backoff

Errors use {"error": {"code": …, "message": …}}. The three key errors include step: public_key_check. Unknown, revoked and other-product keys share public_key_invalid, without exposing ownership information.

Allow roughly 30 seconds for key creation or revocation to propagate. Keys are environment-specific: a production key sent to public.lepsto.dev returns public_key_invalid. The response body does not identify the environment, so verify the host as well as the key. See authentication.

Keep this key out of browsers. Issue a capability token with POST /tokens/issue and return that token to the browser.

CORS

CORS permits https://*.lepsto.dev and the legacy https://*.lessly.dev, which has no removal date. It does not permit your application origins or production .com hosts. Call these keyed HTTP routes from your backend; use delegated tokens for browser sockets.

Body size and rate limits

Limit
Value
Request body, all routes except POST /messages
64 KiB (65 536 bytes)
Request body, POST /messages
256 KiB (262 144 bytes)
Requests
100 per minute, per calling IP address

The publish route allows a larger request body than the other routes. Product publish quotas apply separately from this per-IP request limit; see limits and errors.

POST /tokens/issue

Issue a short-lived token for one authenticated end user. Your backend supplies the subject and allowed channels; the response includes the WebSocket URL.

{
  "subject": "user-8412",
  "channels": [
    { "name": "chat:room-1", "ops": ["subscribe", "history"] },
    { "name": "chat:room-1:typing", "ops": ["subscribe", "publish"] }
  ],
  "ttlSeconds": 900
}
Field
Rules
subject
Required. 1–128 characters of [a-zA-Z0-9._-]. Your own identifier for the user.
channels
Required. 1–32 entries.
channels[].name
Required. A concrete channel name — no wildcards.
channels[].ops
Required. 1–4 of subscribe, publish, presence, history.
ttlSeconds
Optional integer, 60–3600. Defaults to 3600.
{
  "token": "eyJhbGciOiJFZERTQSIs...",
  "gatewayUrl": "wss://<realtime-host>/ws",
  "expiresAt": "2026-08-02T12:15:00.000Z"
}

Return the response to your browser token provider. expiresAt is an ISO-8601 timestamp. Connect to the returned gatewayUrl, which belongs to the environment that issued the token.

Namespace policy removes unsupported operations and channels in unregistered namespaces. If no capabilities remain, issuance returns 422. A successful token can therefore have less access than requested.

POST /messages

Send a JSON value to the channel’s current subscribers:

{
  "channel": "chat:room-1",
  "data": { "text": "hello", "from": "user-8412" }
}

Supply both channel and data. The channel must use namespace:rest, with 3–128 characters from [a-zA-Z0-9._:-]; data can be any JSON value.

{
  "channel": "chat:room-1",
  "published": true,
  "offset": "1754136000000-0",
  "epoch": "17"
}

When history is enabled, offset and epoch locate the retained message. Pass both to GET /messages/history to read subsequent messages.

An unregistered namespace returns 404. Realtime’s payload storage can represent data over 256 KiB as a reference, but the public request-body cap still applies to calls through this route. See limits and errors.

GET /messages/history

Read retained messages using a window or a cursor:

Parameter
Rules
channel
Required.
offset and epoch
A cursor. Both or neither. Returns entries strictly after offset.
last_n
A window. Integer 1–1000. Returns the last N entries.
last_ms
A window. Positive integer. Returns entries newer than now minus this many milliseconds.

A half cursor, a cursor combined with a window, or a request with neither mode returns 400.

GET /messages/history?channel=chat:room-1&last_n=50
{
  "channel": "chat:room-1",
  "epoch": "17",
  "recovered": true,
  "entries": [
    {
      "id": "01J8Z...",
      "data": { "text": "hello" },
      "ts": 1754136000000,
      "offset": "1754136000000-0"
    }
  ]
}

epoch identifies the current history, or is null if no history exists yet. recovered: false means the cursor’s epoch changed or required messages expired. Reload current application state before continuing with live messages.

Each entry includes inline data or a storage ref containing bucket_key, size and optional content_type.

GET /presence

Read the channel’s current members:

GET /presence?channel=chat:room-1
{
  "channel": "chat:room-1",
  "members": [
    {
      "identity": "ext:user-8412",
      "connections": 2,
      "info": { "name": "Ada" },
      "ts": 1754136000000
    }
  ]
}

Each identity appears once. connections counts its live sockets, and ts is the last update in milliseconds. Delegated users retain the token’s ext: prefix, so user-8412 appears as ext:user-8412. Match that returned value; see authentication.

If presence is disabled on the namespace, the read returns 403. These public routes do not enter, update or remove members; browser clients manage their membership over their sockets.

GET /presence/stats

Read the number of distinct identities without downloading the roster:

GET /presence/stats?channel=chat:room-1
{ "channel": "chat:room-1", "members": 12 }

members counts identities, not connections. This route also returns 403 when presence is disabled.

Next steps

Was this page helpful?
Esc

Start typing to search the docs.

↑↓navigate↵select