Console
Docs/Integrate

Player tokens

How your backend lets a player into voice — the one integration point every project has.

The contract#

Your server calls the cell (not the control plane) with the project's SDK key. The cell answers with a session token, the node the player should connect to, and the channels it was granted. The client never sees the key.

Request
POST https://eu-central.cell.aurix.run/v1/tokens        ← your project's cell endpoint (Keys & SDK)
x-api-key: aurx_…
content-type: application/json
Body
{
  "external_id": "player-42",          // your stable player id — one voice user per id
  "display_name": "Ann",
  "channels": [
    { "ad_hoc": { "name": "match-7", "channel_type": "positional" } },
    { "ad_hoc": { "name": "match-7-red", "channel_type": "team" }, "moderate": true },
    { "channel_id": "6f0d…", "speak": false }   // an existing channel, listen-only
  ],
  "region": "eu_central",               // optional: the match's region (cell ids use underscores)
  "location": { "latitude": 52.5, "longitude": 13.4 },   // optional: pick the nearest node
  "client_ip": "203.0.113.9",           // optional: GeoIP fallback when you have no coordinates
  "metadata": { "team": "red" }         // optional: echoed to webhooks and analytics
}

Channel grants#

FieldDefaultMeaning
channel_id or ad_hoc—Existing channel id, or create-on-first-join by name
ad_hoc.channel_type—positional, team, command, whisper, echo
ad_hoc.max_participantsplan limitCap for that channel
jointrueMay join at all
speaktrueMay transmit
receivetrueMay hear
moderatefalseMay mute/kick inside the channel
priorityfalseDucks everyone else while speaking (commander voice)
Response
{
  "token": "eyJ…",                     // pass to the SDK; never log it
  "user_id": "a3c1…",                  // Aurix id for external_id
  "expires_at": "2026-10-21T19:15:00Z",
  "channels": [                        // resolved grants; ad-hoc entries carry their derived id
    { "channel_id": "6f0d…", "join": true, "speak": true, "receive": true, "moderate": false, "priority": false,
      "ad_hoc": { "name": "match-7", "channel_type": "positional" } }
  ],
  "endpoint": {                        // recommended node; null if no node advertises a public URL
    "region": "eu_central",
    "node_id": "…",
    "ws_url": "wss://eu-central.cell.aurix.run/ws",   // signalling — pass to the SDK
    "probe_url": "https://…/probe",     // optional RTT probe for region pickers
    "nodes": 1, "load_factor": 0.12
  },
  "location_source": "coordinates"     // coordinates | geoip | none
}

Hand token, endpoint.ws_url, the cell endpoint (api_url for the Web SDK) and the channel ids to the client. Media addresses (QUIC/UDP, WebTransport, TURN) are negotiated by the SDK after it connects to ws_url; nothing else needs to be configured on the client.

Rules#

  • Tokens are valid for 60 minutes (the cell's auth.token_ttl_secs; there is no per-request TTL) and are only needed to connect: an established session is not cut when its token expires. For reconnects after expiry give the SDK a fresh token — refreshToken (Web), TokenRefresher (Unity), SetToken() (Unreal), set_token() (Godot).
  • One voice user per external_id. A second token for the same id is fine — it is the same user — but a second concurrent session for the same user replaces the first.
  • Ad-hoc channels are created on first use with a deterministic per-project id and removed ten minutes after the last participant leaves; recreate them freely.
  • positional channels only make sense if the client sends positions (updatePosition / UpdateOwnPosition / update_positions in the SDKs) — without them everyone is at the origin and hears everyone.
  • Shared plans (Free, Indie) count sessions across the project; when the cap is reached the token is still issued and the connect is refused with QUOTA_EXCEEDED (HTTP 429 on the REST side) so you can show a message.
  • Rotating the SDK key in the console stops future minting with the old key immediately; issued tokens run to expiry.

Token server#

The server SDKs wrap the cell API with retries, typed errors and webhook verification; the call is the same in every language. Node is on npm (npm i @aurix/server-sdk); Python, Go and C# ship in the release and in sdk/server as path dependencies.

token-server.ts
import { AurixClient } from '@aurix/server-sdk';

const aurix = new AurixClient({ baseUrl: process.env.AURIX_CELL!, apiKey: process.env.AURIX_API_KEY! });

app.post('/voice/token', requirePlayer, async (req, res) => {
  const t = await aurix.issueToken({
    external_id: req.player.id,
    display_name: req.player.name,
    channels: [
      { ad_hoc: { name: `match-${req.body.match}`, channel_type: 'positional' } },
      { ad_hoc: { name: `match-${req.body.match}-${req.player.team}`, channel_type: 'team' } },
    ],
    region: req.body.region,
  });
  res.json(t);   // token, endpoint, channels — safe to return to this player
});
Python
from aurix_server import AurixClient
aurix = AurixClient(os.environ["AURIX_CELL"], api_key=os.environ["AURIX_API_KEY"])
t = aurix.issue_token({"external_id": player.id, "display_name": player.name,
                       "channels": [{"ad_hoc": {"name": f"match-{match}", "channel_type": "team"}}]})
Go
client, _ := aurix.New(aurix.Options{BaseURL: os.Getenv("AURIX_CELL"), Credentials: aurix.Credentials{APIKey: os.Getenv("AURIX_API_KEY")}})
t, err := client.IssueToken(ctx, aurix.GenerateTokenRequest{ExternalID: player.ID, DisplayName: player.Name,
    Channels: []aurix.ChannelGrant{{AdHoc: &aurix.AdHocChannel{Name: "match-" + match, ChannelType: aurix.ChannelTypeTeam}}}})
C#
var aurix = new AurixClient(new AurixClientOptions { BaseUrl = cell, Credentials = new Credentials { ApiKey = apiKey } });
var t = await aurix.IssueTokenAsync(new GenerateTokenRequest { ExternalId = player.Id, DisplayName = player.Name,
    Channels = [new ChannelGrant { AdHoc = new AdHocChannel { Name = $"match-{match}", ChannelType = ChannelType.Team } }] });

Plain HTTPS works from any other language — the full REST reference ships with the engine.

Several regions, one token server#

With one region the token server calls the cell's /v1/tokens with the SDK key, as above. Once a project has more than one region (Regions & routing) let the control plane choose: call POST https://aurix.run/v1/projects/{id}/tokens with an organisation API token (aat_…, Console → Settings → API tokens) and the same body, plus a hint — region (control-plane ids, e.g. eu-central) when the player already picked a shard, or latitude / longitude when you know where they are. Behind a CDN that sets CF-IPCountry (Cloudflare) or CloudFront-Viewer-Country, no hint is needed: the country is enough to pick a continent. The response is the cell's response plus region, cell and basis (what the choice was based on), and endpoint is the nearest node of that cell. Your server never holds backend keys or addresses, and moving a shard between regions is a one-line change on the hint.

js
const r = await fetch(`https://aurix.run/v1/projects/${PROJECT}/tokens`, {
  method: 'POST',
  headers: { authorization: `Bearer ${ORG_API_TOKEN}`, 'content-type': 'application/json' },
  body: JSON.stringify({
    external_id: player.id, display_name: player.name,
    channels: [{ ad_hoc: { name: `shard-${shard}:prox`, channel_type: 'positional' } }],
    region: shardRegion[shard],              // or latitude / longitude, or client_ip (GeoIP on the cell)
  }),
});
const { token, endpoint, region } = await r.json();

GET /v1/projects/{id}/discover answers the same question without minting — useful for a region picker in the lobby (probe_url of each entry can be pinged from the client for real RTT).

Lifetimes and reconnects#

A token is valid for 60 minutes. The SDKs reconnect with the same token for as long as it is valid; after that they ask your refresh callback (or you call SetToken / set_token) and continue the same session — channels are kept. A match longer than an hour therefore needs a reachable token endpoint, not a longer token. Tokens are bound to one project and one cell; they cannot be replayed against another.

Grant recipes#

SituationChannels in the token
Squad / partyone team channel per squad, ad_hoc by match + squad name
Open world, proximityone positional channel per shard, max_participants at the shard size
Leader talks over everyonethe leader gets priority: true on the shared channel
Spectatorspeak: false on every channel
Staff / GMmoderate: true; mute and kick are then available through the SDK
Listen-only broadcastcommand channel with speak: false for the crowd
Mic checkecho channel — the player hears themself with the real pipeline

Grants are per channel; a player can be a moderator in the team channel and listen-only in the shard channel from the same token.

Mistakes we see most#

  • Minting tokens in the client with the SDK key baked in. Rotate the key the moment you notice.
  • A new external_id on every login (random UUIDs) — moderation history and analytics stop lining up; use your account id.
  • One global ad_hoc name like "lobby" — it works until it hits the per-channel participant limit; shard by match or region.
  • Forgetting region for cross-region matches — players then pick by location individually and a match may straddle regions; pass the match's region so everyone lands on one node.

Rotating the SDK key#

Keys & SDK → Rotate issues a new key and revokes the old one immediately. Tokens already issued stay valid until they expire, so rotation is safe mid-game; only your token server needs the new value.