Converse Logo
Guides

Token Authentication

Secure your production integration. Your API key stays on your server — client apps connect with short-lived tokens.

Why Tokens?

API keys grant full account access. If you put one in a browser or mobile app, anyone can extract it. Instead, your backend generates short-lived tokens (expire in 5 minutes) that can only do one thing: connect a voice or chat session.

The Flow

User taps "Call" Your app asks YOUR server for a token

Your server Converse API (with API key) returns short-lived token

Your server returns token to app

App connects to Converse voice/chat with token (no API key needed)

Server: Generate Token (Node.js)

typescript
import { expressTokenHandler } from '@converse/sdk/mobile';
import express from 'express';

const app = express();
app.use(express.json());
app.post('/api/voice-token', expressTokenHandler({
  apiKey: process.env.CONVERSE_API_KEY!,
  allowedAgentIds: ['agent_xxx'],
  authorize: request => isSignedIn(request),
}));

Server: Generate Token (Python)

python
from converse import AsyncConverseClient

@app.post("/api/voice-token")
async def voice_token(request: Request):
    body = await request.json()
    if body["agentId"] not in ALLOWED_AGENT_IDS:
        raise HTTPException(403)
    async with AsyncConverseClient(api_key=os.environ["CONVERSE_API_KEY"]) as client:
        session = await client.calls.create_web_call(body["agentId"])
    return {"token": session["token"], "serverUrl": session["server_url"], "callId": session["call_id"]}

Client: Use tokenProvider

React (Web)

tsx
import { useConverseVoice, ConverseOrb } from '@converse/sdk/components';

function Agent() {
  const { agentState, voiceState, connect, disconnect } = useConverseVoice({
    agentId: 'agent_xxx',
    tokenProvider: async () => {
      const res = await fetch('/api/voice-token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ agentId: 'agent_xxx' }),
      });
      return res.json(); // { token, serverUrl, callId }
    },
  });

  return <ConverseOrb state={agentState} onClick={voiceState === 'idle' ? connect : disconnect} />;
}

React Native

tsx
import { useConverseVoice } from '@converse/react-native';

const { connect } = useConverseVoice({
  agentId: 'agent_xxx',
  tokenProvider: async () => {
    const res = await fetch('https://yourapi.com/api/voice-token', {
      method: 'POST',
      body: JSON.stringify({ agentId: 'agent_xxx' }),
    });
    return res.json();
  },
});

iOS (Swift)

swift
let config = ConverseConfig(
    voiceTokenProvider: {
        let (data, _) = try await URLSession.shared.data(for: tokenRequest)
        return try JSONDecoder().decode(VoiceToken.self, from: data)
    }
)
let voice = ConverseVoice(config: config)
try await voice.connect(agentId: "agent_xxx")

Never use apiKey in production

Never place a secret sk_* key in client code. Use an origin-restricted publishable key or a server-side tokenProvider.