Get ICE servers / credentials
The one endpoint you need. Authenticate with your project API key, get back a short-lived credential pair plus a ready-to-use multi-transport iceServers array.
Endpoint
Call this server-side, authenticated with the project API key. Forward only the result to the browser.
Request
| Header / field | Type | Description |
|---|---|---|
Authorization | header | Bearer <YOUR_API_KEY> — the project key from your dashboard. |
Content-Type | header | application/json |
ttl | number | Lifetime of the returned credential in seconds. Defaults to 3600 (1 hour). Mint a fresh pair before it expires. |
region | string | Which region the relay should run in — one of the slugs in Regions below. Must be enabled on your project. |
Regions
Pass one of these slugs as region. The response echoes back the region the relay was allocated in.
| Slug | Location |
|---|---|
us-east | United States — East |
us-west | United States — West |
eu-central | European Union — Frankfurt |
eu-west | European Union — Ireland |
uk-london | United Kingdom — London |
in-mumbai | India — Mumbai |
ap-singapore | Singapore |
ap-sydney | Australia — Sydney |
Need a region that isn't listed? Ask us — we add regions on request.
curl -X POST https://api.usebaton.io/v1/credentials \
-H "Authorization: Bearer $BATON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "ttl": 3600, "region": "eu-central" }'// server-side only — never expose BATON_API_KEY to the browser
const res = await fetch("https://api.usebaton.io/v1/credentials", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.BATON_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ ttl: 3600, region: "eu-central" })
});
const creds = await res.json();
// return creds.iceServers (+ username/credential) to your clientimport os, requests
r = requests.post(
"https://api.usebaton.io/v1/credentials",
headers={{"Authorization": f"Bearer {os.environ['BATON_API_KEY']}"}},
json={{"ttl": 3600, "region": "eu-central"}},
timeout=5,
)
creds = r.json() # forward creds["iceServers"] to the browserbody := strings.NewReader(`{"ttl":3600,"region":"eu-central"}`)
req, _ := http.NewRequest("POST", "https://api.usebaton.io/v1/credentials", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("BATON_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
// decode resp.Body, forward iceServers to the clientResponse
You get back one credential pair, reused across all transport URLs. The client's ICE agent tries them in parallel and keeps whichever connects first — you don't pick a transport.
{
"username": "1718200000:project_ab12",
"credential": "<ephemeral-credential>",
"ttl": 3600,
"region": "eu-central",
"iceServers": [
{
"urls": [
"stun:relay.usebaton.io:3478",
"turn:relay.usebaton.io:3478?transport=udp",
"turn:relay.usebaton.io:3478?transport=tcp",
"turns:relay.usebaton.io:443?transport=tcp"
],
"username": "1718200000:project_ab12",
"credential": "<ephemeral-credential>"
}
]
}Response fields
| Field | Description |
|---|---|
username | <unix-expiry>:<project> — the credential is valid until the Unix timestamp encoded in the prefix. |
credential | The ephemeral credential. Pair it with username. |
ttl | Seconds the credential remains valid from issue. |
region | The region the relay was allocated in. |
iceServers | Drop-in array for RTCPeerConnection. Same username/credential on every URL. |
Why all four URLs are returned
| URL | Role |
|---|---|
stun:…:3478 | Gather server-reflexive candidates; connect directly when possible — no relay. |
turn:…:3478?transport=udp | Preferred relay path — lowest latency, best quality. |
turn:…:3478?transport=tcp | Fallback when UDP is blocked. |
turns:…:443?transport=tcp | TURN over TLS on 443 — looks like HTTPS, traverses strict DPI / deny-all corporate and hospital firewalls. The endpoint that matters most for enterprise clients. |
Credential model
Baton issues a short-lived ephemeral token — an opaque username/credential pair. Your server fetches it from the credentials API with your project API key; you don't derive or compute it. The username encodes the token's expiry (<unix-expiry>:<project>), and the credential is the matching secret for that single token.
- One model, no exceptions. A long-lived API key (server-side) is exchanged for a short-lived ephemeral token. The client only ever sees the token. There is no shared secret and no static credential.
- Mint per session, server-side. Call the endpoint when you need a token and hand only the result to the browser. The API key never reaches the client.
- TTL & expiry. The token stops working at the Unix time in its
username. Request a fresh one before then — for long calls, refresh ahead of expiry. - Clock skew. The relay validates the expiry against its own clock. Keep your servers on NTP; if a token is rejected as expired right after issue, suspect a skewed client/server clock.
Use the credentials on the client
// iceServers came from YOUR server (which called Baton)
const pc = new RTCPeerConnection({
iceServers,
iceTransportPolicy: "all" // use "relay" to force-test the relay path
});// Option A — set TURN on the LiveKit server (livekit.yaml)
rtc:
turn_servers:
- host: relay.usebaton.io
port: 443
protocol: tls
username: "1718200000:project_ab12"
credential: "<ephemeral-credential>"
// Option B — pass iceServers from the client at connect time
await room.connect(url, token, {
rtcConfig: { iceServers }
});