Secure

Publisher tokens

Let a device publish with a token your backend mints, or an encoder with a stream key, and rotate or revoke either.

Two kinds of source publish to a stream, and each has its own credential:

SourceCredentialHow it arrivesMade by
An encoder (OBS, ffmpeg, a hardware encoder)stream key, stk_...in the SRT or RTMP address, to our ingestPOST /v1/streams, once
Your own device or program, with the SDKpublisher tokento the relay, as the session's credentialPOST /v1/streams/{id}/publisher-tokens, as often as you like

A stream's ingest setting says which it takes (rtmp, rtmps, srt, whip for an encoder; sdk for a device). A stream can take both. Viewers get their own tokens: Viewer tokens.

Publisher tokens

A publisher token is a short signed token for one stream. The relay checks it when the device connects, and closes the device's session when the token runs out (Token renewal). It lets the holder do three things on that stream's namespace, and nothing else: publish, read back its own heartbeat track (the relay's liveness echo), and receive commands on the stream's control namespace. It does not let the device watch the stream.

Your backend mints it with your secret key:

curl -s $API/v1/streams/$ID/publisher-tokens -H "Authorization: Bearer $SK" \
  -d '{"ttl":86400,"subject":"camera-7","token_id":true}'
{
  "token": "0oRIogEm...",
  "token_id": "pub-a181e5cd97b266b2816557ed3f316b1d",
  "expires_at": "2026-10-27T19:10:48Z",
  "exp": 1793128248,
  "relays": [{ "url": "https://relay.nearglass.com/tn_9a1..." }],
  "namespace": "live/st_8f2c...",
  "catalog": "catalog"
}

Status 201. The body is JSON; an empty body is {}, and a field the call does not know is refused with 400.

FieldDefaultWhat
ttl3600the token's life in seconds, 1 to 604800 (7 days)
subjectthe stream idwho the token is for; it shows in the relay's records as device:<subject> (cut to 64 characters). No secrets
token_idfalsegive the token an id: the relay then allows one live session for it, and we can close it (below). token_id in the answer is null without it

Errors are { "error": { "code": ..., "message": ... } }:

StatuscodeWhen
400invalid_requestttl of 0 or over 604800, an unknown field, a body that is not JSON
401unauthorizedno key, an unknown key, or a publishable key
404not_foundno such stream, or not yours
503clock_unsynchronizedthe API's clock is not synchronized (Time sync)

Real answers from a test stack:

{"error":{"code":"invalid_request","message":"ttl: 1 to 604800 s"}} [400]
{"error":{"code":"invalid_request","message":"body: unknown field `ttl_s`, expected one of `ttl`, `subject`, `token_id` at line 1 column 8"}} [400]
{"error":{"code":"unauthorized","message":"unknown key"}} [401]
{"error":{"code":"not_found","message":"no such stream"}} [404]

The call does not check the stream's ingest list: a token can be minted for a stream that lists only rtmp. The publish.sdk block that POST /v1/streams returns (the relays, the namespace and token_endpoint) is there only when ingest contains sdk.

Mint one in your backend

Node 20 or later, no packages:

const API = process.env.NEARGLASS_API; // https://api.nearglass.com
const KEY = process.env.NEARGLASS_SECRET_KEY; // sk_...

export async function publisherToken(streamId, { ttl = 3600, subject, tokenId = false } = {}) {
  const r = await fetch(`${API}/v1/streams/${streamId}/publisher-tokens`, {
    method: 'POST',
    headers: { authorization: `Bearer ${KEY}`, 'content-type': 'application/json' },
    body: JSON.stringify({ ttl, ...(subject ? { subject } : {}), token_id: tokenId }),
  });
  const body = await r.json();
  if (r.status !== 201) throw new Error(`publisher-tokens: HTTP ${r.status} ${body.error?.code}: ${body.error?.message}`);
  return body; // { token, token_id, exp, expires_at, relays, namespace, catalog }
}

Python 3, standard library only:

import json, os, urllib.error, urllib.request

API = os.environ["NEARGLASS_API"]  # https://api.nearglass.com
KEY = os.environ["NEARGLASS_SECRET_KEY"]  # sk_...


def publisher_token(stream_id, ttl=3600, subject=None, token_id=False):
    body = {"ttl": ttl, "token_id": token_id}
    if subject:
        body["subject"] = subject
    req = urllib.request.Request(
        f"{API}/v1/streams/{stream_id}/publisher-tokens",
        data=json.dumps(body).encode(),
        headers={"authorization": f"Bearer {KEY}", "content-type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req) as r:
            return json.load(r)  # token, token_id, exp, expires_at, relays, namespace, catalog
    except urllib.error.HTTPError as e:
        err = json.load(e).get("error", {})
        raise RuntimeError(f"publisher-tokens: HTTP {e.code} {err.get('code')}: {err.get('message')}") from None

Both ran against a test stack and returned the same fields; a missing stream raised publisher-tokens: HTTP 404 not_found: no such stream, and a ttl of 0 raised the 400 above.

Getting the token to the device

Keep sk_... on your servers. The device gets a token from your backend, the way a browser gets a viewer token from your token-url route. The relay checks the token when the device connects, so a token handed over once is good for as long as its ttl: choose a ttl for how long the device may go without reaching your backend, up to 7 days.

What the tools do with it today:

  • Nearglass publish --api https://api.nearglass.com --secret-key-file sk.txt --stream st_... --token-ttl 3600 asks the API for a fresh token at every connect, so a long-running publisher renews itself. It holds the secret key, so it suits your own machines (an encoder box you run, a test rig), not a device you hand to someone else.
  • nearglass publish --descriptor file.json and --relay ... --token-file read one token when the command starts and use it for every reconnect until it expires. After that the relay refuses the device.
  • The C library (nearglass.h) takes one token when you create the handle (nearglass_conn.token). Create a new handle with a fresh token before the old one expires.

Stream keys

An encoder cannot mint tokens, so it sends a stream key. Our ingest checks the key and publishes to the relay for the encoder, with a publisher token of its own that never leaves the ingest.

  • A key is stk_ and 22 characters. POST /v1/streams returns it once, in publish.stream_key, with the ready-made SRT and RTMP addresses that carry it. We keep a hash only: a lost key cannot be shown again, only replaced. GET /v1/streams/{id} says has_stream_key and shows no key.
  • A stream whose ingest is only sdk has no stream key.

Rotate a key

curl -s -X POST $API/v1/streams/$ID/stream-key -H "Authorization: Bearer $SK"
{
  "id": "st_8f2c...",
  "publish": {
    "stream_key": "stk_...",
    "srt": "srt://ingest.nearglass.com:9000?streamid=stk_...",
    "rtmp": "rtmp://ingest.nearglass.com:1935/live/stk_..."
  }
}

Status 200. The new key is shown once. What happens to the old one:

  • The API forgets the old key at once. The ingest syncs its list of keys every 3 s, and then refuses new connections on the old key and ends an encoder already sending on it.
  • Every publisher token of that stream that you minted with token_id: true is closed at the relay. A token minted without token_id cannot be closed: its session goes on until the token expires.
  • 409 no_stream_key for a stream whose ingest is only sdk.

Run on a test stack: a device publishing with a token_id: true token was closed by the rotation (the relay's reason: "closed by admin") and its later connects were refused ("token closed"); a device with a token minted without token_id stayed connected through the rotation.

Put the new address into the encoder, then restart it. OBS: Settings, Stream.

Cut a device off

You wantDoEffect
a rotated stream keyPOST /v1/streams/{id}/stream-keythe encoder is out; tokens minted with token_id: true are closed
a device with a token minted with token_id: true gone nowrotate the stream's key (a stream that lists an encoder ingest), or delete the streamits session closes at once
a device gone, and you can waitstop minting for itit is out when its token expires, at most ttl later
everything on a stream goneDELETE /v1/streams/{id}stops admitting, and closes every session of the stream, viewers and publisher alike

There is no call that revokes one publisher token by itself. A stream that lists only sdk has no key to rotate, so for that stream the choices are a short ttl or deleting the stream. Keep ttl short for devices you may need to cut off.

One token, one session

A token minted with token_id: true allows one live session at a time. A device that drops and reconnects with the same token within about 10 s (the relay's idle timeout, while the old session lingers) can be refused as "token in use"; ask for a new token for each connect instead. A token without an id can be shared by reconnects, which is why it is the default for a device that keeps one token.

Reference: POST /v1/streams/{id}/publisher-tokens, POST /v1/streams/{id}/stream-key.

On this page