WebRTC

TURN/STUN credentials for peer-to-peer video streaming

WebRTC API

Retrieve credentials for STUN/TURN servers to enable peer-to-peer video streaming.

Base URL: | WebSocket:

Get TURN Credentials

Retrieve temporary TURN server credentials for WebRTC connections.

Endpoint: GET /api/turn-credentials

Authentication Required — This endpoint requires a valid session cookie. Only authenticated Cliqer users can retrieve TURN credentials.
interface TurnCredentials {
  username: number
  password: string
  ttl: number
  uris: string[]
}

const credentials: TurnCredentials = await $fetch('/api/turn-credentials')

Response:

{
  "username": 1705319400,
  "password": "base64encodedhmac==",
  "ttl": 86400,
  "uris": [
    "stun:stun.cloudflare.com:3478",
    "turn:turn.cliqer.io:3478?transport=udp",
    "turn:turn.cliqer.io:3478?transport=tcp",
    "turns:turn.cliqer.io:443?transport=tcp"
  ]
}

Using TURN Credentials

Browser (JavaScript)

const credentials = await $fetch('/api/turn-credentials')

const peerConnection = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.cloudflare.com:3478' },
    {
      urls: 'turns:turn.cliqer.io:443',
      username: credentials.username.toString(),
      credential: credentials.password
    }
  ]
})

peerConnection.onicecandidate = (event) => {
  if (event.candidate) {
    ws.send(JSON.stringify({
      type: 'webrtcSignal',
      payload: {
        room: roomId,
        signal: { type: 'candidate', candidate: event.candidate }
      }
    }))
  }
}

WebRTC Signaling via WebSocket

Cliqer uses a perfect negotiation pattern for WebRTC signaling. The flow:

  1. Non-initiator sends ready signal to announce presence
  2. Initiator creates offer upon receiving ready
  3. Non-initiator responds with answer
  4. Both exchange ICE candidates

Signal Types

TypeSenderPurpose
readyNon-initiatorAnnounce ready to receive offer
offerInitiatorSDP offer for connection
answerNon-initiatorSDP answer to offer
candidateBothICE candidate for NAT traversal
// Non-initiator signals ready
ws.send(JSON.stringify({
  event: 'message',
  payload: {
    type: 'webrtcSignal',
    payload: {
      room: 'ABC123',
      signal: { type: 'ready' }
    }
  }
}))

// Initiator sends offer
ws.send(JSON.stringify({
  event: 'message',
  payload: {
    type: 'webrtcSignal',
    payload: {
      room: 'ABC123',
      signal: {
        type: 'offer',
        sdp: peerConnection.localDescription?.sdp
      }
    }
  }
}))

// Handle incoming signals
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  if (msg.type === 'webrtcSignal') {
    const { signal, from } = msg.payload
    
    switch (signal.type) {
      case 'ready':
        // Create and send offer (if initiator)
        break
      case 'offer':
        await peerConnection.setRemoteDescription(signal)
        const answer = await peerConnection.createAnswer()
        await peerConnection.setLocalDescription(answer)
        // Send answer back
        break
      case 'answer':
        await peerConnection.setRemoteDescription(signal)
        break
      case 'candidate':
        await peerConnection.addIceCandidate(signal.candidate)
        break
    }
  }
}

Server Configuration

ServerURLPortProtocolPurpose
Cloudflare STUNstun:stun.cloudflare.com3478UDPPublic IP discovery
Cliqer STUNstun:rtc.cliqer.io3478UDPNAT traversal
Cliqer TURN (UDP)turn:turn.cliqer.io3478UDPRelay (fastest)
Cliqer TURN (TCP)turn:turn.cliqer.io3478TCPRelay (firewall-friendly)
Cliqer TURNSturns:turn.cliqer.io443TLS/TCPSecure relay fallback

Credential Lifetime

  • TTL: 86400 seconds (24 hours)
  • Rotation: Credentials rotate automatically via HMAC
  • Algorithm: SHA-1 HMAC signed with server secret

Rate Limiting & Security

EndpointAuthRequests/MinuteBlock Duration
/api/turn-credentialsRequired105-60 minutes (progressive)
TURN credentials require authentication and are rate-limited by IP address. Unauthenticated requests return 401. Excessive requests result in temporary blocks.

Copyright © 2026. All rights reserved.