From 3a176c3d2e3c5e39f698ec2f787aa710af3467f0 Mon Sep 17 00:00:00 2001 From: Ryan Newton + Claude Date: Mon, 1 Dec 2025 19:30:35 +0000 Subject: [PATCH 1/2] Improve voice token error handling with detailed ElevenLabs messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse ElevenLabs API error responses to surface actual error messages - Helps debug issues like missing API key permissions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sources/app/api/routes/voiceRoutes.ts | 71 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/sources/app/api/routes/voiceRoutes.ts b/sources/app/api/routes/voiceRoutes.ts index ddc2842..05d05ce 100644 --- a/sources/app/api/routes/voiceRoutes.ts +++ b/sources/app/api/routes/voiceRoutes.ts @@ -2,12 +2,20 @@ import { z } from "zod"; import { type Fastify } from "../types"; import { log } from "@/utils/log"; +/** + * Voice routes for ElevenLabs Conversational AI integration. + * + * Required environment variables: + * - ELEVENLABS_API_KEY: Your ElevenLabs API key + * - ELEVENLABS_AGENT_ID: Your ElevenLabs Conversational AI agent ID + * + * In development mode (ENV=dev), RevenueCat subscription check is skipped. + */ export function voiceRoutes(app: Fastify) { app.post('/v1/voice/token', { preHandler: app.authenticate, schema: { body: z.object({ - agentId: z.string(), revenueCatPublicKey: z.string().optional() }), response: { @@ -24,7 +32,7 @@ export function voiceRoutes(app: Fastify) { } }, async (request, reply) => { const userId = request.userId; // CUID from JWT - const { agentId, revenueCatPublicKey } = request.body; + const { revenueCatPublicKey } = request.body; log({ module: 'voice' }, `Voice token request from user ${userId}`); @@ -33,7 +41,7 @@ export function voiceRoutes(app: Fastify) { // Production requires RevenueCat key if (!isDevelopment && !revenueCatPublicKey) { log({ module: 'voice' }, 'Production environment requires RevenueCat public key'); - return reply.code(400).send({ + return reply.code(400).send({ allowed: false, error: 'RevenueCat public key required' }); @@ -54,20 +62,18 @@ export function voiceRoutes(app: Fastify) { if (!response.ok) { log({ module: 'voice' }, `RevenueCat check failed for user ${userId}: ${response.status}`); - return reply.send({ - allowed: false, - agentId + return reply.send({ + allowed: false }); } const data = await response.json() as any; const proEntitlement = data.subscriber?.entitlements?.active?.pro; - + if (!proEntitlement) { log({ module: 'voice' }, `User ${userId} does not have active subscription`); - return reply.send({ - allowed: false, - agentId + return reply.send({ + allowed: false }); } } @@ -75,11 +81,18 @@ export function voiceRoutes(app: Fastify) { // Check if 11Labs API key is configured const elevenLabsApiKey = process.env.ELEVENLABS_API_KEY; if (!elevenLabsApiKey) { - log({ module: 'voice' }, 'Missing 11Labs API key'); - return reply.code(400).send({ allowed: false, error: 'Missing 11Labs API key on the server' }); + log({ module: 'voice' }, 'Missing ELEVENLABS_API_KEY environment variable'); + return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing API key)' }); + } + + // Check if agent ID is configured + const agentId = process.env.ELEVENLABS_AGENT_ID; + if (!agentId) { + log({ module: 'voice' }, 'Missing ELEVENLABS_AGENT_ID environment variable'); + return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing agent ID)' }); } - // Get 11Labs conversation token + // Get 11Labs conversation token (for WebRTC connections) const response = await fetch( `https://api.elevenlabs.io/v1/convai/conversation/token?agent_id=${agentId}`, { @@ -90,19 +103,41 @@ export function voiceRoutes(app: Fastify) { } } ); - + if (!response.ok) { - log({ module: 'voice' }, `Failed to get 11Labs token for user ${userId}`); - return reply.code(400).send({ + const errorText = await response.text(); + log({ module: 'voice' }, `Failed to get 11Labs token: ${response.status} ${errorText}`); + + // Parse error for better user feedback + let errorDetail = 'Failed to get voice token from ElevenLabs'; + try { + const errorJson = JSON.parse(errorText); + if (errorJson.detail?.message) { + errorDetail = errorJson.detail.message; + } else if (errorJson.detail?.status) { + errorDetail = `ElevenLabs error: ${errorJson.detail.status}`; + } + } catch { + // Use default error message + } + + return reply.code(400).send({ allowed: false, - error: `Failed to get 11Labs token for user ${userId}` + error: errorDetail }); } const data = await response.json() as any; - console.log(data); const token = data.token; + if (!token) { + log({ module: 'voice' }, 'ElevenLabs returned empty token'); + return reply.code(400).send({ + allowed: false, + error: 'ElevenLabs returned invalid response' + }); + } + log({ module: 'voice' }, `Voice token issued for user ${userId}`); return reply.send({ allowed: true, From 071da4482610c0a4db6e4fa2a66ba04f0c583680 Mon Sep 17 00:00:00 2001 From: Ryan Newton + Claude Date: Mon, 1 Dec 2025 20:11:22 +0000 Subject: [PATCH 2/2] Support custom ElevenLabs credentials in voice token endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Accept optional customAgentId and customApiKey in request body - Use user-provided credentials when available, fallback to server env vars - Enables per-user ElevenLabs agent configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- sources/app/api/routes/voiceRoutes.ts | 48 ++++++++++++++++++--------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/sources/app/api/routes/voiceRoutes.ts b/sources/app/api/routes/voiceRoutes.ts index 05d05ce..68b1e07 100644 --- a/sources/app/api/routes/voiceRoutes.ts +++ b/sources/app/api/routes/voiceRoutes.ts @@ -5,9 +5,9 @@ import { log } from "@/utils/log"; /** * Voice routes for ElevenLabs Conversational AI integration. * - * Required environment variables: - * - ELEVENLABS_API_KEY: Your ElevenLabs API key - * - ELEVENLABS_AGENT_ID: Your ElevenLabs Conversational AI agent ID + * Supports two modes: + * 1. Server credentials (default): Uses ELEVENLABS_API_KEY and ELEVENLABS_AGENT_ID env vars + * 2. User credentials: Client provides customAgentId and customApiKey in the request * * In development mode (ENV=dev), RevenueCat subscription check is skipped. */ @@ -16,7 +16,10 @@ export function voiceRoutes(app: Fastify) { preHandler: app.authenticate, schema: { body: z.object({ - revenueCatPublicKey: z.string().optional() + revenueCatPublicKey: z.string().optional(), + // Custom ElevenLabs credentials (when user provides their own) + customAgentId: z.string().optional(), + customApiKey: z.string().optional() }), response: { 200: z.object({ @@ -32,7 +35,7 @@ export function voiceRoutes(app: Fastify) { } }, async (request, reply) => { const userId = request.userId; // CUID from JWT - const { revenueCatPublicKey } = request.body; + const { revenueCatPublicKey, customAgentId, customApiKey } = request.body; log({ module: 'voice' }, `Voice token request from user ${userId}`); @@ -78,18 +81,31 @@ export function voiceRoutes(app: Fastify) { } } - // Check if 11Labs API key is configured - const elevenLabsApiKey = process.env.ELEVENLABS_API_KEY; - if (!elevenLabsApiKey) { - log({ module: 'voice' }, 'Missing ELEVENLABS_API_KEY environment variable'); - return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing API key)' }); - } + // Determine which credentials to use: user-provided or server defaults + const useCustomCredentials = customAgentId && customApiKey; + + let elevenLabsApiKey: string | undefined; + let agentId: string | undefined; + + if (useCustomCredentials) { + // User provided their own ElevenLabs credentials + log({ module: 'voice' }, `Using custom ElevenLabs credentials for user ${userId}`); + elevenLabsApiKey = customApiKey; + agentId = customAgentId; + } else { + // Use server's default credentials + elevenLabsApiKey = process.env.ELEVENLABS_API_KEY; + agentId = process.env.ELEVENLABS_AGENT_ID; + + if (!elevenLabsApiKey) { + log({ module: 'voice' }, 'Missing ELEVENLABS_API_KEY environment variable'); + return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing API key)' }); + } - // Check if agent ID is configured - const agentId = process.env.ELEVENLABS_AGENT_ID; - if (!agentId) { - log({ module: 'voice' }, 'Missing ELEVENLABS_AGENT_ID environment variable'); - return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing agent ID)' }); + if (!agentId) { + log({ module: 'voice' }, 'Missing ELEVENLABS_AGENT_ID environment variable'); + return reply.code(400).send({ allowed: false, error: 'Voice not configured on server (missing agent ID)' }); + } } // Get 11Labs conversation token (for WebRTC connections)