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:
| Source | Credential | How it arrives | Made by |
|---|---|---|---|
| An encoder (OBS, ffmpeg, a hardware encoder) | stream key, stk_... | in the SRT or RTMP address, to our ingest | POST /v1/streams, once |
| Your own device or program, with the SDK | publisher token | to the relay, as the session's credential | POST /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.
| Field | Default | What |
|---|---|---|
ttl | 3600 | the token's life in seconds, 1 to 604800 (7 days) |
subject | the stream id | who the token is for; it shows in the relay's records as device:<subject> (cut to 64 characters). No secrets |
token_id | false | give 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": ... } }:
| Status | code | When |
|---|---|---|
| 400 | invalid_request | ttl of 0 or over 604800, an unknown field, a body that is not JSON |
| 401 | unauthorized | no key, an unknown key, or a publishable key |
| 404 | not_found | no such stream, or not yours |
| 503 | clock_unsynchronized | the 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 NoneBoth 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 3600asks 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.jsonand--relay ... --token-fileread 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/streamsreturns it once, inpublish.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}sayshas_stream_keyand shows no key. - A stream whose
ingestis onlysdkhas 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: trueis closed at the relay. A token minted withouttoken_idcannot be closed: its session goes on until the token expires. 409 no_stream_keyfor a stream whoseingestis onlysdk.
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 want | Do | Effect |
|---|---|---|
| a rotated stream key | POST /v1/streams/{id}/stream-key | the encoder is out; tokens minted with token_id: true are closed |
a device with a token minted with token_id: true gone now | rotate the stream's key (a stream that lists an encoder ingest), or delete the stream | its session closes at once |
| a device gone, and you can wait | stop minting for it | it is out when its token expires, at most ttl later |
| everything on a stream gone | DELETE /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.