Skip to content
ngTurn

Time-limited TURN credentials

Never ship a TURN password inside a web or mobile app: anyone can copy it and relay traffic on your account. Your backend asks us for credentials instead, and hands the result to the device.

  1. Your backend calls POST https://api.ngturn.io/v1/credentials with your API key.
  2. We mint a short-lived username and password and return them as a ready-made iceServers list.
  3. Your backend passes that list to the device, which uses it to open a peer connection.
  4. The credential stops working for new connections once it expires. A call already running is not cut off.

The API key stays on your server and is never sent to a device. The credentials we return are safe to hand out: they expire on their own, they are tied to your account, and you can revoke the key behind them at any time.

turn-credentials.js
export async function turnCredentials({ ttl = 43200, region = 'auto' } = {}) {
const res = await fetch('https://api.ngturn.io/v1/credentials', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.NGTURN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ttl, region }),
});
if (!res.ok) throw new Error(`ngTurn credentials failed: ${res.status}`);
return res.json(); // { iceServers, ttl, expiresAt }
}
{
"iceServers": [
{ "urls": ["stun:relay.ngturn.io:3478"] },
{
"urls": [
"turn:relay.ngturn.io:3478?transport=udp",
"turn:relay.ngturn.io:3478?transport=tcp",
"turns:relay.ngturn.io:443?transport=tcp"
],
"username": "1790000000:acc_7Qm2x.key_Hk29d",
"credential": "…"
}
],
"ttl": 43200,
"expiresAt": "2026-10-01T12:00:00Z"
}

Pass iceServers straight into new RTCPeerConnection({ iceServers }). The list is already ordered so the fastest path is tried first.

Field Default Notes
ttl 43200 (12 hours) Seconds. Maximum 24 hours
region auto auto picks the nearest relay. eu or us pins every client to one region, for data residency or firewall allowlisting

Make the lifetime longer than your longest expected call. A credential is checked when the connection is set up, so a call that starts inside the window runs to its end either way — but an ICE restart part-way through needs one that is still valid.

Fetch once and reuse until roughly half the lifetime has passed, then fetch again. With the 12-hour default that is one call to us every six hours, not one per user and not one per connection.

If a relay becomes unreachable mid-call, the connection does not recover on its own. Fetch fresh credentials and perform an ICE restart:

ice-restart.js
pc.addEventListener('connectionstatechange', async () => {
if (pc.connectionState === 'failed') {
const { iceServers } = await turnCredentials();
pc.setConfiguration({ iceServers });
await pc.restartIce();
}
});