Tokens

TokenRequest, TokenDetails and the Blackevin JWT — what your server signs, what Blackevin issues, and what the client sends.

Three things with similar names

what it iswho makes it
TokenRequesta signed ask for a tokenyour server, locally
TokenDetailsthe answer, wrapping the tokenBlackevin
Blackevin JWTthe credential itselfBlackevin

The usual flow is: your server signs a TokenRequest, the browser exchanges it for TokenDetails, and the JWT inside is what connects. The SDK does the last two steps for you if you give it authUrl — see authentication.

TokenRequest

{
  "keyName": "k1",
  "ttl": 3600000,
  "capability": "{\"chat:*\":[\"subscribe\",\"publish\"]}",
  "clientId": "alice",
  "timestamp": 1710000000000,
  "nonce": "0123456789abcdef",
  "mac": "<base64 HMAC-SHA256>"
}

The MAC is computed over these six fields in this order, UTF-8, with a newline after each one including empty ones:

keyName
ttl
capability
clientId
timestamp
nonce

Two things people get wrong, both of which produce the same unhelpful invalid token request mac:

  • ttl is milliseconds. An Ably-shaped 1.hour in seconds reads as 3.6 seconds here.
  • capability is signed as a JSON string, not as an object. Sign the exact bytes you send.

timestamp must be within ±2 minutes of the server's clock, and nonce must be unique per key and timestamp. Both are replay defences: a captured TokenRequest is useless two minutes later, and useless twice.

const rest = new Blackevin.Rest({ key: process.env.BLACKEVIN_KEY });

const tokenRequest = await rest.auth.createTokenRequest({
  clientId: 'alice',
  ttl: 3_600_000,
  capability: { 'chat:*': ['subscribe', 'publish'] },
});

createTokenRequest needs key, so it only runs somewhere you trust with one. It signs locally and makes no network call — your login path must not depend on Blackevin being reachable.

Exchanging it

POST /keys/{keyName}/requestToken

This endpoint takes no Authorization header: the TokenRequest is itself signed, and that signature is the credential.

An unsigned body is also accepted, with Authorization: Basic base64(key) — useful from a server that already holds the key and has no reason to HMAC anything.

TokenDetails

{
  "token": "<Blackevin JWT>",
  "keyName": "k1",
  "issued": 1710000000000,
  "expires": 1710003600000,
  "capability": "{\"chat:*\":[\"subscribe\",\"publish\"]}",
  "clientId": "alice"
}

From the SDK:

const details = await realtime.auth.requestToken({ clientId: 'alice' });

The Blackevin JWT

HS256, signed with the API key secret, with the key id in the header so the node knows which secret to verify against.

header kidthe keyId
iat, expunix seconds, both required
x-blackevin-capabilitythe capability map, as a JSON string. Required
x-blackevin-client-idoptional. When present it is forced on the connection

Custom Blackevin names are lowercase kebab-case throughout — claims, AMQP headers, HTTP extensions: x-blackevin-<words-with-hyphens>.

A token's capabilities must be a subset of the issuing key's. Asking for more is refused at issue time, which names the key rather than the session.

x-blackevin-client-id being forced is what makes presence trustworthy: a client cannot claim to be someone else, because the identity was decided by whoever signed the token.

Signing one directly

TokenRequest is the flow for browsers. A server that just wants a scoped token can skip it:

import { signBlackevinJwt } from '@blackevin/core';

const jwt = signBlackevinJwt({
  keyName: 'k1',
  secret: process.env.BLACKEVIN_SECRET,
  capabilities: { 'chat:*': ['subscribe', 'publish', 'presence'] },
  clientId: userId,
  expiresIn: 3600,
});

expiresIn is seconds here, matching exp. The millisecond ttl belongs to TokenRequest and only to TokenRequest.

Sending it

ws://host/?accessToken=<jwt>
Authorization: Bearer <jwt>

token is accepted as a query alias for accessToken.

The SDK builds both from whatever you passed — key gives you Basic and the key query, token gives you Bearer and accessToken.

Refreshing mid-session

A connection can swap its credential without dropping:

{ "op": 15, "id": "1", "accessToken": "…" }

The server answers Ack and a fresh Connected. You will not normally write this yourself: with authUrl or authCallback the SDK re-fetches on every reconnect already.

Not the console token

Two unrelated HS256 JWTs live in this system and they share nothing but the algorithm name.

this page — data planethe console — control plane
held bya customer's browser or clienta human signed into /app
signed withthe API key secretthe console's own secret
identified byheader kidiss: "clowk"
carriesx-blackevin-capability, x-blackevin-client-idsub, email, session_id

Never pass a console token to a Blackevin client, and never let an API key secret verify a console token.

On this page